Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ed55a3420 | ||
|
|
8184585213 |
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
status: active
|
||||||
|
initiated: 2026-10-03
|
||||||
|
touches: [the seats, the seat protocol, the controller's ownership check, 03-DESIGN/01-to-be/26-the-seats.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 023 — A seat protocol that defines what its holder owns
|
||||||
|
|
||||||
|
## What is investigated
|
||||||
|
|
||||||
|
**A seat is a definition — a protocol — and a module occupies it by implementing that protocol.**
|
||||||
|
Today the protocol is what the holder accepts, emits and serves (ADR 0118, 0129, 0132): its verbs, as MCP
|
||||||
|
tool definitions. This asks whether the protocol should also name the **files and directories the
|
||||||
|
holder owns**, so that occupying the seat means owning them: `node-resolver-config` owns
|
||||||
|
`/etc/resolv.conf`, `node-hosts-file` owns `/etc/hosts`, the intrusion prevention owns its jail file.
|
||||||
|
|
||||||
|
The direction is the protocol's, not the holder's: the seat states what any holder must own; a module
|
||||||
|
that wants the seat must declare those paths among its resources, or the controller refuses the claim
|
||||||
|
as not implementing the seat. Two seats may not name one path.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Who owns a singular file is today answered by reading every manifest, and enforced only after the fact,
|
||||||
|
when two modules on one machine both declare the same path. The question *which module owns
|
||||||
|
`/etc/resolv.conf`?* came up on 2026-10-03 with no place to look it up. A seat that names the path answers
|
||||||
|
it from the seat table, before any module is written, and makes "implements the seat" checkable.
|
||||||
|
|
||||||
|
## What it touches
|
||||||
|
|
||||||
|
- The seat definition and its table (ADR 0122) — a new part of the protocol.
|
||||||
|
- The controller's ownership check (`checkResources`), which already refuses two modules owning one path.
|
||||||
|
- Every node seat that is really about a file: `node-resolver-config`, `node-hosts-file`
|
||||||
|
([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)),
|
||||||
|
`node-intrusion-prevention`, `node-packet-filter`.
|
||||||
|
|
||||||
|
Raised by the operator during the resolver work of ADRs 0194–0199 and parked there so that work was not
|
||||||
|
widened by it.
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
topic: the tiers
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-03
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 199. A module that answers names declares its zone, and a node's hosts file is one module's
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**[ADR 0194](0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md) and
|
||||||
|
[ADR 0196](0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md) leave
|
||||||
|
one resolver holding the nodes' internal domains, and retire the resolver every node ran.** Two kinds
|
||||||
|
of names lived in those per-node resolvers that are neither a node nor a route, and both were found on
|
||||||
|
the workstation on 2026-10-03:
|
||||||
|
|
||||||
|
- **Names a module answers.** The lab raises scenario machines and gives them addresses from its
|
||||||
|
scenario files — the anchor's stand-in at a documentation address, the home server's on the LAN —
|
||||||
|
and the workstation resolved `<machine>.incus` through two wildcard lines in a drop-in file its
|
||||||
|
resolver read. The lines were written by hand; the addresses are the lab's, known only while a
|
||||||
|
scenario runs.
|
||||||
|
- **The operator's own names, unrelated to the mesh.** Twelve `<loopback> <name>` lines for a
|
||||||
|
client's development hosts, kept in `/etc/hosts` and again in `/etc/hosts.local`, which the per-node
|
||||||
|
resolver read as additional hosts.
|
||||||
|
|
||||||
|
**A manifest never names an address, a node or a domain** ([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)).
|
||||||
|
So the lab cannot list `<machine>.incus → <address>` in its definition, and the operator's twelve lines
|
||||||
|
are not any module's to define.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**For a module's names:**
|
||||||
|
|
||||||
|
1. **The manifest lists its records.** Refused by ADR 0112: the addresses are the lab's runtime facts
|
||||||
|
and the scenario's choice.
|
||||||
|
2. **The module reports its records at runtime to the mesh's resolver**, which writes them into its
|
||||||
|
configuration. It works, and it makes the resolver hold every module's runtime state and decide,
|
||||||
|
per call, whether the caller may write the name it sent — authorisation for a write, on the one
|
||||||
|
server every node depends on.
|
||||||
|
3. **The module declares the zone it answers and the listen that answers it; the mesh's resolver
|
||||||
|
forwards that zone there.** The definition names a zone (from a setting) and one of its own listens,
|
||||||
|
which ADR 0112 allows; the address and the port are the mesh's facts. The records stay where they
|
||||||
|
are known — in the module, at runtime. Chosen.
|
||||||
|
|
||||||
|
**For the operator's names:**
|
||||||
|
|
||||||
|
1. **Records the controller holds, served by the mesh's resolver.** They are not the mesh's: a client's
|
||||||
|
development hosts on one machine are nothing any other node should resolve, and the controller would
|
||||||
|
become the keeper of a workstation's private notes.
|
||||||
|
2. **A node-scoped module owns `/etc/hosts`, and the operator's lines live in its kept region**
|
||||||
|
([ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)), changed
|
||||||
|
through that module's tools on that machine. Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. A module that answers names declares a zone.** Its definition names the zone — a single label or a
|
||||||
|
dotted name, from a setting, never a domain the mesh knows — and the listen that answers DNS for it.
|
||||||
|
The controller refuses two modules in the mesh declaring one zone, and a zone that is the mesh's suffix,
|
||||||
|
under it, or one of a node's public domains: a module may not shadow names the mesh or the public DNS
|
||||||
|
answers.
|
||||||
|
|
||||||
|
**2. The mesh's resolver forwards each zone to the module that declared it.** The controller hands the
|
||||||
|
holder of `mesh-dns-resolver` every declared zone with the private address of the node its module runs
|
||||||
|
on and the port that listen is published on; the holder places one forwarding rule per zone into its
|
||||||
|
configuration and answers nothing in that zone itself. What names exist in the zone, and their
|
||||||
|
addresses, are the module's — answered by its own long-running code
|
||||||
|
([ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)),
|
||||||
|
from its own state, as they change. Whether an answered address is reachable from the asking node is
|
||||||
|
the module's matter, not the resolver's.
|
||||||
|
|
||||||
|
**3. A node's `/etc/hosts` is held by one module, through a node seat, `node-hosts-file`.** The seat is
|
||||||
|
the definition: its holder owns `/etc/hosts`, and implements three verbs — MCP tool definitions served
|
||||||
|
as `<node>/node-hosts-file.<verb>` ([ADR 0195](0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)):
|
||||||
|
**`entries`** (the file's lines, the module's and the operator's, each marked whose), **`add`** (one
|
||||||
|
address and its names, into the operator's region) and **`remove`** (one name or address from it). The
|
||||||
|
module writes the machine's own lines — loopback and the machine's name — and keeps a region for the
|
||||||
|
operator, which survives every push and is given back when the module goes. Its tools change that
|
||||||
|
region on that machine, escalating as the packet filter's do
|
||||||
|
([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) §4). **The
|
||||||
|
controller holds none of it:** an operator's line is the machine's, not a record.
|
||||||
|
|
||||||
|
**4. No other module writes `/etc/hosts`.** The private network's region goes, as ADR 0194 already has
|
||||||
|
it; a module that once wrote a line there asks the mesh's resolver instead.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **The lab's names follow its scenarios.** A scenario raised is resolvable from every node at once; a
|
||||||
|
scenario torn down is gone, with no line left behind in any file.
|
||||||
|
- **The mesh's resolver holds no module's state.** It holds the nodes' domains and a table of who
|
||||||
|
answers which zone, both composed by the controller; nothing writes to it at runtime.
|
||||||
|
- **A module answering a zone needs a DNS answerer of its own** — a long-running bundle, or a resolver
|
||||||
|
it runs. The lab gains one.
|
||||||
|
- **The operator's names reach the machine's own programs, not its containers.** A container does not
|
||||||
|
read the machine's `/etc/hosts`. For names unrelated to the mesh that is the right boundary; a name a
|
||||||
|
container needs belongs in a zone.
|
||||||
|
- **Taking `/etc/hosts` keeps what is there.** The first time the module writes the file, every line
|
||||||
|
that is not the machine's own goes into the operator's region, so a workstation's twelve lines survive
|
||||||
|
the take — the same adoption [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) gives
|
||||||
|
every shared file.
|
||||||
|
|
||||||
|
**How each is checked:**
|
||||||
|
|
||||||
|
- **Zones:** the controller's catalogue tests refuse a second module declaring a zone, a zone under the
|
||||||
|
mesh suffix, and a zone equal to a node's public domain.
|
||||||
|
- **Forwarding:** on the holder, the resolver's configuration carries one forwarding rule per declared
|
||||||
|
zone, at the declaring node's private address and published port; asking any node's resolver for a
|
||||||
|
name in the lab's zone while a scenario runs returns the scenario's address.
|
||||||
|
- **The hosts file:** a push leaves the operator's region byte for byte; `add` followed by `entries`
|
||||||
|
shows the line as the operator's; unassigning the module gives the region back.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0194](0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md),
|
||||||
|
[ADR 0196](0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md) —
|
||||||
|
the one resolver and how nodes ask it.
|
||||||
|
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — why a definition names no address.
|
||||||
|
- [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md),
|
||||||
|
[ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) — kept regions and shared files.
|
||||||
|
- [ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md) —
|
||||||
|
where a zone's answerer runs.
|
||||||
|
- [Connectivity §2](../03-DESIGN/01-to-be/08-connectivity.md) and [the seats](../03-DESIGN/01-to-be/26-the-seats.md),
|
||||||
|
amended alongside.
|
||||||
|
- [Research 023](../01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md) —
|
||||||
|
the general form of decision 3's "the holder owns `/etc/hosts`".
|
||||||
@@ -225,6 +225,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0191** — [The mesh's resolver holds only the mesh's own names; a public name resolves publicly](0191-the-meshs-resolver-holds-only-the-meshs-own-names.md)
|
- **0191** — [The mesh's resolver holds only the mesh's own names; a public name resolves publicly](0191-the-meshs-resolver-holds-only-the-meshs-own-names.md)
|
||||||
- **0194** — [The mesh has one resolver, and every node asks it for the mesh's names](0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md)
|
- **0194** — [The mesh has one resolver, and every node asks it for the mesh's names](0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md)
|
||||||
- **0196** — [A node asks the mesh's resolver first, and a public one only when it is silent](0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md)
|
- **0196** — [A node asks the mesh's resolver first, and a public one only when it is silent](0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md)
|
||||||
|
- **0199** — [A module that answers names declares its zone, and a node's hosts file is one module's](0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)
|
||||||
|
|
||||||
### What runs on them, and how it gets there
|
### What runs on them, and how it gets there
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ code:
|
|||||||
- mesh-controller examples/route-proxy
|
- mesh-controller examples/route-proxy
|
||||||
- mesh-controller internal/identity/authority.go
|
- mesh-controller internal/identity/authority.go
|
||||||
- mesh-controller cmd/mesh-controller/plan.go (the names the roster publishes)
|
- mesh-controller cmd/mesh-controller/plan.go (the names the roster publishes)
|
||||||
|
- mesh-controller internal/catalogue/zones.go (the zones a module answers, ADR 0199)
|
||||||
|
- mesh-controller internal/catalogue/seats.go (mesh-dns-resolver, node-hosts-file)
|
||||||
|
- mesh-catalog modules/dnsmasq (the mesh's one resolver)
|
||||||
|
- mesh-catalog modules/resolv-conf (what a node asks)
|
||||||
|
- mesh-catalog modules/hosts (a node's /etc/hosts)
|
||||||
- mesh-host internal/identity/serving.go
|
- mesh-host internal/identity/serving.go
|
||||||
- mesh-host internal/apply (the service that reflects a rule set)
|
- mesh-host internal/apply (the service that reflects a rule set)
|
||||||
updated: 2026-10-03
|
updated: 2026-10-03
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md
|
||||||
- 02-DECISIONS/0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md
|
- 02-DECISIONS/0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md
|
||||||
- 02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md
|
- 02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md
|
||||||
- 02-DECISIONS/0191-the-meshs-resolver-holds-only-the-meshs-own-names.md
|
- 02-DECISIONS/0191-the-meshs-resolver-holds-only-the-meshs-own-names.md
|
||||||
@@ -377,6 +383,16 @@ member's resolver answers a LAN; a router pointing at one is moved first. *Check
|
|||||||
`/etc/resolv.conf` naming `mesh-resolver` then a public resolver, by no node but the holder answering
|
`/etc/resolv.conf` naming `mesh-resolver` then a public resolver, by no node but the holder answering
|
||||||
DNS on any address, and by the router's DHCP DNS option naming the router.*
|
DNS on any address, and by the router's DHCP DNS option naming the router.*
|
||||||
|
|
||||||
|
**Names that are neither a node nor a route.** A module that answers names declares a zone (a
|
||||||
|
setting) and the listen that answers it; the controller hands the `mesh-dns-resolver` holder every
|
||||||
|
zone with its module's node address and published port, and the holder forwards that zone there and
|
||||||
|
answers nothing in it itself — the lab answers `<machine>.incus` for its running scenarios this way.
|
||||||
|
An operator's own names, unrelated to the mesh, live in `/etc/hosts`'s kept region, held per node by
|
||||||
|
the `node-hosts-file` seat's holder and changed through its tools; the controller holds none of them
|
||||||
|
([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)).
|
||||||
|
*Checked by the holder's configuration carrying one forwarding rule per declared zone, and by a push
|
||||||
|
leaving the hosts file's operator region byte for byte.*
|
||||||
|
|
||||||
*What follows describes the per-node resolver this replaces — how it was built and why the roles were
|
*What follows describes the per-node resolver this replaces — how it was built and why the roles were
|
||||||
split. The split stands; the serving role's scope is what moved.*
|
split. The split stands; the serving role's scope is what moved.*
|
||||||
|
|
||||||
@@ -1021,6 +1037,8 @@ The list is worth having in one place, because it is most of the argument:
|
|||||||
|
|
||||||
- **One resolver ([ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md)).** Not built: every node still runs
|
- **One resolver ([ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md)).** Not built: every node still runs
|
||||||
`node-dns-resolver`. The migration's four steps are in the record, in order.
|
`node-dns-resolver`. The migration's four steps are in the record, in order.
|
||||||
|
Nor are zones or the hosts file's holder ([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)): the
|
||||||
|
workstation moves to the one resolver only once both exist, its lab and operator names depending on them.
|
||||||
|
|
||||||
- ~~**What happens when the hub is down.**~~ **Resolved** by
|
- ~~**What happens when the hub is down.**~~ **Resolved** by
|
||||||
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md), together with `06`'s
|
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md), together with `06`'s
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ code:
|
|||||||
- mesh-catalog modules/gitea/module.json
|
- mesh-catalog modules/gitea/module.json
|
||||||
updated: 2026-10-03
|
updated: 2026-10-03
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md
|
||||||
- 02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md
|
- 02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md
|
||||||
- 02-DECISIONS/0161-what-deserves-a-seat.md
|
- 02-DECISIONS/0161-what-deserves-a-seat.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||||
@@ -129,6 +130,7 @@ convention, which later seats departed from.
|
|||||||
| `mesh-build-machine` | `the-build-machine` | node | — | a builder |
|
| `mesh-build-machine` | `the-build-machine` | node | — | a builder |
|
||||||
| `mesh-resolver` | — | mesh | — | the mesh's one resolver, holding every node's internal domain ([ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md)) |
|
| `mesh-resolver` | — | mesh | — | the mesh's one resolver, holding every node's internal domain ([ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md)) |
|
||||||
| ~~`mesh-dns-port`~~ | `the-dns-port` | node | — | retired by [ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md): the local resolver became the mesh's one |
|
| ~~`mesh-dns-port`~~ | `the-dns-port` | node | — | retired by [ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md): the local resolver became the mesh's one |
|
||||||
|
| `node-hosts-file` | — | node | — | owns `/etc/hosts`: the machine's own lines and the operator's kept region, changed through its verbs `entries`, `add`, `remove` ([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)) |
|
||||||
| `mesh-intrusion-prevention` | `the-intrusion-prevention` | node | — | an intrusion-prevention service |
|
| `mesh-intrusion-prevention` | `the-intrusion-prevention` | node | — | an intrusion-prevention service |
|
||||||
| `mesh-packet-filter` | `the-packet-filter` | node | — | the packet filter |
|
| `mesh-packet-filter` | `the-packet-filter` | node | — | the packet filter |
|
||||||
| `mesh-private-network` | `the-private-network` | node | — | the private network the mesh runs over |
|
| `mesh-private-network` | `the-private-network` | node | — | the private network the mesh runs over |
|
||||||
|
|||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
# 211 — Diagnosis
|
|
||||||
|
|
||||||
*2026-10-03.* The planner orders a merge's modules by `inventory.Dependencies`, whose edges come from a
|
|
||||||
manifest's `build.on`, from what a build recorded it stood on, from the repositories it read, and from
|
|
||||||
the build machine. A bundle names its toolchain by `language`; the builder takes the toolchain image
|
|
||||||
(`ToolchainFor(language)`) from what the mesh holds and records nothing of it as stood on. So no edge
|
|
||||||
ran from a bundle to the module publishing its toolchain, and a merge moving both (mesh-tools: the
|
|
||||||
images and node-tools) tiered them together. **Fix (mesh-controller, branch
|
|
||||||
`fix/issue-211-a-bundle-stands-on-its-toolchain`, commit c72f6ca):** `dependenciesOf` adds a `stands-on`
|
|
||||||
edge from every bundle artifact to its toolchain's module, read from the manifest. Tested: TypeScript
|
|
||||||
bundle → mesh-tools, Go bundle → mesh-tools-go, image → none; a merge moving both plans two tiers.
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: open
|
||||||
opened: 2026-10-03
|
opened: 2026-10-03
|
||||||
located-in:
|
located-in:
|
||||||
- mesh-controller
|
- mesh-controller
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
# 214 — Diagnosis
|
|
||||||
|
|
||||||
*2026-10-03.* A plan learns a tier's outcome only through `planBuilt`, called when a controller takes
|
|
||||||
in a build result off the bus. A merge to the controller's repository replaces the controller in tier
|
|
||||||
0; the build that produced the new controller was recorded, but the plan state the new controller
|
|
||||||
loaded still read `asked`, and no path ever revisited it. **Fix (branch
|
|
||||||
`fix/issue-214-a-plan-settles-from-the-build-records`, commit d86baeb):** `advanceOnce` settles every
|
|
||||||
still-asked module from its build records — a build recorded after the ask is that ask's outcome,
|
|
||||||
built or failed — on every advance and on the 30-second ticker. Tested with a pure helper. Live
|
|
||||||
proof: the next merge to mesh-controller passes tier 0 on its own.
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: open
|
||||||
opened: 2026-10-03
|
opened: 2026-10-03
|
||||||
located-in:
|
located-in:
|
||||||
- mesh-controller
|
- mesh-controller
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
# 215 — Diagnosis
|
|
||||||
|
|
||||||
*2026-10-03.* `takeIn` registers a build's `Ref` as the branch the module follows. unifi was once
|
|
||||||
built with `ref=9c97a8a`, which became its followed ref. `sourceIs` matches a merge only to modules
|
|
||||||
whose ref is empty or the merged base — so every merge into main left unifi out — and `askTier`
|
|
||||||
re-asks `Source.Ref`, so every plan that rebuilt unifi built the same old commit again (its build
|
|
||||||
records all read "at 9c97a8a"). **Fix (branch `fix/issue-215-a-commit-is-never-a-branch-to-follow`,
|
|
||||||
commit 6784efa):** registration keeps the followed branch when a build names a commit; matching and
|
|
||||||
re-asking read a recorded commit as the default branch, healing existing records; a merge names the
|
|
||||||
modules of its repository it leaves out. Store-backed test fails without the fix.
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: open
|
||||||
opened: 2026-10-03
|
opened: 2026-10-03
|
||||||
located-in:
|
located-in:
|
||||||
- mesh-controller
|
- mesh-controller
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
# 216 — Diagnosis
|
|
||||||
|
|
||||||
*2026-10-03.* The composer delivers a bundle as an archive only when its `Loads` is non-empty, and
|
|
||||||
`Loads` derives from the artifact's `loads` or, failing that, from the module's `tools` list. The
|
|
||||||
seven modules had neither, so their bundles were recorded and never composed into any declaration;
|
|
||||||
nothing checked it. **Fix (branch `fix/issue-216-a-bundle-nothing-delivers-is-refused`, commit
|
|
||||||
cf2bb3b):** registration refuses a bundle that nothing loads, runs or unpacks — no `loads`, no `tools`,
|
|
||||||
no resource naming it, and not the runtime — naming the field that would deliver it. The current
|
|
||||||
catalogue passes the check.
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
---
|
|
||||||
status: located
|
|
||||||
opened: 2026-10-03
|
|
||||||
located-in:
|
|
||||||
- mesh-tools
|
|
||||||
fixed-by:
|
|
||||||
amended-design:
|
|
||||||
---
|
|
||||||
|
|
||||||
# 217 — A refused announcement took down every container's runtime, and the console with it
|
|
||||||
|
|
||||||
## What was observed
|
|
||||||
|
|
||||||
2026-10-03, rolling out [ADR 0197](../../02-DECISIONS/0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md).
|
|
||||||
The tool runtimes announced themselves by subscribing `$SRV.<verb>.>`; the grants composed for them
|
|
||||||
allowed only `$SRV.<verb>` and the service's own name and instance. The bus refused the wildcard:
|
|
||||||
|
|
||||||
```
|
|
||||||
Subscription Violation - User "novox.gitea", Subject "$SRV.PING.>"
|
|
||||||
NatsError: 'Permissions Violation for Subscription to "$SRV.PING.>"'
|
|
||||||
```
|
|
||||||
|
|
||||||
The TypeScript runtime the per-module containers run treats a refused subscription as fatal, so on
|
|
||||||
the one machine that had received the new images nine containers crash-looped — gitea's runtime,
|
|
||||||
postgres's, the catalogue, the vault, mongodb, mssql, keycloak, mailu, nextcloud. Gitea's tools went
|
|
||||||
with them, which closed the usual path for merging the fix.
|
|
||||||
|
|
||||||
Then the console stopped answering the controller's verbs, though the controller held every
|
|
||||||
subscription: the console builds the list that tells a seat's verb from a module's tool by asking the
|
|
||||||
controller *and* the catalogue, and gives up on both when the catalogue does not answer — so
|
|
||||||
`mesh-controller.status` was asked of a module subject nobody serves.
|
|
||||||
|
|
||||||
## Why it matters beyond this instance
|
|
||||||
|
|
||||||
Two properties, each worse than the mistake that exposed it:
|
|
||||||
|
|
||||||
- **A runtime dies for an optional subscription.** Announcing is discovery; serving tools and running
|
|
||||||
provisioners is the work. A refusal of the first should never stop the second.
|
|
||||||
- **The console's view of the mesh's own verbs depended on a module.** The controller's verbs are how
|
|
||||||
the operator repairs the mesh; they must not become unreachable because the catalogue is down.
|
|
||||||
|
|
||||||
## Diagnosis
|
|
||||||
|
|
||||||
Owner mesh-tools. The wildcard is fixed on branch `fix/announce-only-what-the-grants-allow`: both
|
|
||||||
runtimes subscribe exactly what the grants allow. The console that discovers from what announces itself
|
|
||||||
(ADR 0195, 0197, on main) asks the bus and the controller, not the catalogue, which removes the second
|
|
||||||
property once it is deployed. **Still to do:** the TypeScript runtime treats a refused announcement
|
|
||||||
subscription as a logged warning, not as fatal; a test against a bus with real grants proves the
|
|
||||||
announcement subscriptions are allowed for every principal kind.
|
|
||||||
|
|
||||||
Recovered on the day without the forge's API: the toolchain images built by the controller straight
|
|
||||||
from the fix branch, every module image rebuilt on them, and the machine pushed.
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
---
|
|
||||||
status: located
|
|
||||||
opened: 2026-10-03
|
|
||||||
located-in:
|
|
||||||
- mesh-controller
|
|
||||||
fixed-by: mesh-controller#248
|
|
||||||
amended-design:
|
|
||||||
---
|
|
||||||
|
|
||||||
# 218 — A seat held once for the mesh is answered by a module on a machine that does not hold it
|
|
||||||
|
|
||||||
## What was observed
|
|
||||||
|
|
||||||
2026-10-03. Asked which databases the mesh's store holds, `mesh-store.databases` answered from the
|
|
||||||
postgres on one machine with that machine's application databases; the controller's own database
|
|
||||||
lives on the postgres of the other machine, which the controller's records name as the seat's one
|
|
||||||
holder:
|
|
||||||
|
|
||||||
```
|
|
||||||
mesh-store scope: mesh delivers: postgres-database holders: [ {node: <the control machine>, module: postgres} ]
|
|
||||||
```
|
|
||||||
|
|
||||||
The discovery console, which reads what answers on the bus ([ADR 0197](../../02-DECISIONS/0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)),
|
|
||||||
shows the same seat announced from **both** machines running postgres.
|
|
||||||
|
|
||||||
## Why it matters beyond this instance
|
|
||||||
|
|
||||||
A seat held once for the mesh promises one answerer: the role's holder. A module that implements a
|
|
||||||
seat's verbs on every machine it runs on, and is let serve them on each, turns "the mesh's store" into
|
|
||||||
"whichever postgres replied first" — a read against the wrong database that looks like a right one,
|
|
||||||
and a write would be worse. Every mesh-scoped seat whose implementing module runs on more than one
|
|
||||||
machine has this shape.
|
|
||||||
|
|
||||||
## Where to look
|
|
||||||
|
|
||||||
Whether the runtime serves a seat's verbs where its module merely *claims* the seat rather than where
|
|
||||||
the mesh made it the holder ([ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md),
|
|
||||||
[ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):
|
|
||||||
the membership the controller issues each assignment, and what the runtime admits from it. A check:
|
|
||||||
a mesh-scoped seat's verbs are served by exactly the holder the records name, on every machine.
|
|
||||||
|
|
||||||
## Root cause
|
|
||||||
|
|
||||||
The controller composed each assignment's held seats from what its module *claims*, once per module
|
|
||||||
and not once per machine. Every machine running postgres was therefore given the store seat's grants
|
|
||||||
and issued its subjects, and each runtime served the seat's verbs because it serves what it is issued
|
|
||||||
([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)).
|
|
||||||
The runtime behaved as designed. The fault was in what it was issued.
|
|
||||||
|
|
||||||
The seat's verbs are not the module's tools. The store's `databases` and `query` are a separate
|
|
||||||
implementation registered under the seat's name ([ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)).
|
|
||||||
Only that implementation should be withdrawn where the module does not hold the seat. postgres's own
|
|
||||||
tools stay served on every machine it runs on.
|
|
||||||
|
|
||||||
## Fix
|
|
||||||
|
|
||||||
The controller now reads the recorded seat holdings when it composes grants and memberships. A seat
|
|
||||||
held once for the mesh is issued only to the machine and module the records name as its holder. A
|
|
||||||
seat held once per machine, and a mesh seat with no holder on record, are issued as before. Grants
|
|
||||||
and memberships come from the same list, so they cannot disagree.
|
|
||||||
|
|
||||||
**How it is checked.** A controller test asserts that a claimant on another machine keeps its node
|
|
||||||
seats and loses the recorded mesh seat. Live, the discovery console's overview must show each
|
|
||||||
mesh-scoped seat announced from exactly the holder the records name. Status moves to `resolved` once
|
|
||||||
that holds after the fix is rolled out.
|
|
||||||
Reference in New Issue
Block a user