Compare commits

..
Author SHA1 Message Date
jschoubben 98eb3aa76f ADR 0169 → 0170: the firewall seat's record renumbered after a collision on main; its built note; cycle.py refuses two records sharing a number
Another session's 0169 landed first. The collision check from issue 155
covered issue folders only; it covers decision records now, and would have
refused this.
2026-10-02 14:51:22 +02:00
mesh-admin 91bbe648a8 Merge pull request 'Issues 199 (resolved: a node-scoped seat's verb through the console) and 200 (the controller's answer to a long console call is refused by the bus)' (#288) from issues/199-200-console-seat-verbs-and-inbox into main 2026-10-02 12:25:37 +00:00
jschoubben 0e7b85f184 Issues 199 (resolved: a node-scoped seat's verb through the console) and 200 (the controller's answer to a long console call is refused by the bus) 2026-10-02 14:25:03 +02:00
mesh-admin dac49de6e7 Merge pull request 'ADR 0172: the lab is a module, and runs a bed when the mesh asks' (#287) from jschoubben/the-lab-is-a-module into main 2026-10-02 12:17:21 +00:00
jschoubben 0d9208dbbf ADR 0172: the lab is a module, and runs a bed when the mesh asks 2026-10-02 14:15:44 +02:00
mesh-admin bf0ee7cb25 Merge pull request 'ADR 0169: the firewall seat serves its verbs, and a foreign rule set is removed through one of them' (#285) from feat/the-firewall-seat-serves-its-verbs into main 2026-10-02 11:29:15 +00:00
jschoubben 4567e13071 ADR 0169: the firewall seat serves its verbs, and a foreign rule set is removed through one of them
Designs 33 and 08 revised. The first node-scoped seat with verbs: rules,
reload, remove; the nftables module holds it from a runtime with NET_ADMIN,
the first container to declare a capability.
2026-10-02 13:27:03 +02:00
mesh-admin aa5d9f1045 Merge pull request 'ADR 0169 (proposed): a machine joins through the tunnel, and the bus is never public' (#284) from jschoubben/a-machine-joins-through-the-tunnel into main 2026-10-02 11:17:42 +00:00
9 changed files with 293 additions and 4 deletions
+16
View File
@@ -131,6 +131,22 @@ def main():
else:
seen[number] = name
# And decision records, which 155's fix left out: on 2026-10-02 two ADRs numbered 0169 landed
# on main from two sessions within the hour, and every check passed.
seen_records = {}
for path in sorted(glob.glob(os.path.join(ROOT, "02-DECISIONS", "[0-9]*.md"))):
name = os.path.basename(path)
number = name.split("-", 1)[0]
if not number.isdigit():
continue
if number in seen_records:
bad(os.path.join("02-DECISIONS", name),
"is numbered %s, and so is %s -- a record's number is how it is cited. Take the next "
"free number across main AND every open pull request; the branch that lands last "
"renumbers" % (number, seen_records[number]))
else:
seen_records[number] = name
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
front = frontmatter(path)
if front is None:
@@ -0,0 +1,103 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
---
# 170. The firewall seat serves its verbs, and a foreign rule set is removed through one of them
## Context
[ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) made the mesh say truthfully
what filters a converged machine, and left the removal of what it did not write to the operator's
hand. The first time that hand was needed — two machines, five rule sets a predecessor and a
retired front end had left — there was no mesh way to lend it: the packet filter is a seat
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)), a seat's
holder serves its verbs ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md),
[ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)), and the
firewall seat declared none. The only remaining path was a shell on the machine, which is the path
the mesh exists to replace, and which the operator's own tooling rightly refused to an agent.
A seat's verbs are the contract every holder implements, whatever filter it speaks. What a person
asks a machine's packet filter is the same whether nftables, a front end or a legacy filter answers:
what are the rules, reload the mesh's own, remove this thing the mesh did not write. What differs by
filter is the holder's own business and may be its own tools beside the seat's.
## Decision
**1. The `node-packet-filter` seat serves three verbs**, and a module that claims it serves all
three or is refused the claim, as with every seat:
- `rules` — the packet filter as the machine enforces it now: the nftables ruleset, and the legacy
filter's listings where that tool exists; narrowed to one table or chain when asked. Read-only.
- `reload` — load the mesh's own filter again from the file the mesh writes, and answer with the
mesh's table as loaded. The holder's own act on the holder's own rules.
- `remove` — remove one rule set the mesh did not write, named exactly as the host reports it under
ADR 0168 (`chain HAL-MESH-ONLY (iptables-legacy)`, `table ip6 filter, chain DOCKER-USER`), and
answer with what was done. It refuses the mesh's own tables, the container runtime's own chains,
a built-in chain other than the runtime's user chain, and any chain of a found firewall that is
in force. The runtime's user chain is emptied back to its one return; another chain loses the
jumps into it, is flushed and deleted; a table of the machine's own is deleted whole. Each is an
operator's act, by name, on one thing the mesh reported — never a flush, never a rule the mesh
itself marked.
**2. A holder may serve its own tools beside the seat's.** The nftables module keeps its reading of
the mesh's table as its own tool, and a holder speaking a filter with specifics of its own may add
tools for them; the seat's three are what every holder owes.
**3. A container may ask for a capability.** Serving `remove` and `reload` needs the machine's
network namespace and the right to change its packet filter; a holder's runtime declares
`capabilities: ["NET_ADMIN"]` on its container and runs on the machine's network. The host grants
exactly the capabilities declared, names them in the container's spec so a change recreates it, and
refuses a name that is not a capability's. A privileged container stays undeclarable.
**4. ADR 0168's "by hand" is read as "by the operator, through the seat".** Removing what the mesh
reports as *other* is still the operator's act and is still never the mesh's own doing; the verb is
how the act reaches the machine, recorded on the bus like every other, instead of a shell.
## Consequences
- The seat's row gains the three verbs; a mesh that already runs widens its row at the next
controller start. The nftables module claims them and gains a runtime — a tool server with the
packet filter's tools in its image, on the machine's network, with `NET_ADMIN`.
- The host's container vocabulary grows by `capabilities`; an older host refuses a declaration that
carries it, so the host rolls before the module.
- The two machines of this mesh that ADR 0168 found not filtered by the mesh alone are cleaned
through `remove`, and read *the mesh alone* afterwards; `status` returns to well without a hand on
either machine.
## How this is checked
| Rule | Checked by |
|---|---|
| The seat declares the three verbs; a claim that serves fewer is refused by name | the catalogue's seat tests |
| `remove` refuses the mesh's tables, the runtime's chains, a built-in chain and an active front end's chains, and removes a user chain with its jumps, empties the user chain, deletes an own table | the module's tests over a fake command runner, with the shapes the host reported live |
| A container's capabilities reach the runtime and its spec; an unknown name is refused | host tests |
| Live | `node-packet-filter.remove@<node>` on the home server and the control node; `node show` reads *the mesh alone* on both; `status` is well |
## Built and proven live, 2026-10-02
> **Progressive insight — 2026-10-02.** The decision stands; these are the facts of its building.
> Written as 0169 for three hours and renumbered to 0170: another record took 0169 on main first,
> and the check that refuses a shared number covered issues only (now records too).
Built in mesh-host 68 (`capabilities` on a container), mesh-controller 212 (the seat's three verbs)
and 213 (the filter file a module names under `filtering.into` counts as declared for a mount — the
module's first build was refused without it), mesh-catalog 216 (the nftables module's runtime and
verbs) and mesh-tools 27 (the console lists a node-scoped seat's verbs with their scope and carries the
machine; before it, the verbs were live on four machines and unreachable from the console —
[issue 199](../04-ISSUES/199-a-node-scoped-seats-verb-could-not-be-called-through-the-console/00-report.md)).
Each machine's holder was issued its bus account with `mesh-controller.issue`, the broker node pushed
first. At 12:46Z the five rule sets ADR 0168 had named were removed through
`node-packet-filter.remove`, three on the home server and two on the control node, each answering
with the commands it ran; the next report read *the mesh alone* on all four machines and `status`
listed nothing under `filtered`. The live row is read. What it cost on the way is
[issue 200](../04-ISSUES/200-the-controllers-answer-to-the-console-is-refused-by-the-bus/00-report.md).
## References
- [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md)
@@ -0,0 +1,59 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0016-the-lab.md
---
# 172. The lab is a module, and runs a bed when the mesh asks
## Context
The lab raises virtual machines and runs the mesh on them, end to end, before a change reaches a real
machine ([ADR 0016](0016-the-lab.md)). It runs on one machine of the mesh, the one with the
virtualisation it needs. Until now the only way to start a bed there was to sign in to that machine and
run the lab's command line by hand, with a dozen environment variables pointing at sibling checkouts.
Nothing in the mesh could ask for it. An agent working through the mesh's own tools could build,
merge and push a change, and could not prove it in the lab first. The operator's direction on
2026-10-02: work on another machine goes through a mesh tool, not a shell on it.
## Considered Options
1. **Keep the lab a command line on one machine.** Every run is a person, or an agent with a shell on
that machine, outside the mesh.
2. **The lab is a module.** Assigned to the machine that can run it, serving tools that run a bed
against named branches and say how it went.
## Decision
**Option 2.**
- **A `lab` module, assigned where the lab can run**, serves five tools: whether this machine can run
beds, run beds against a branch per repository, a run's state, its log, and stopping it.
- **A run is the lab's own suite**, against fresh checkouts of the named branches from the mesh's forge,
side by side as the lab expects them. It builds what the beds place from those checkouts, as the suite
already does. It answers at once with an id, like a build: a bed takes minutes, and a call does not.
- **Only branches on the forge are run**, never code handed to the tool. What a run tested is what the
forge holds at the commit it names.
- **The lab is reached over the mesh only.** Its tools travel the bus, and the module opens no port.
- **No grant beyond the mesh's own.** Running a bed is root on the lab's machine, but anyone who can call
the mesh's tools can already do worse. The operator's judgement on 2026-10-02.
## Consequences
- An agent proves a change in the lab through the mesh, the same way it builds and pushes one.
- The lab's machine carries a module whose runtime holds the virtualisation's and the container
runtime's sockets, and a toolchain to build the mesh with.
- A run's checkouts are its own, so two runs never build from each other's tree. Old ones are removed
when their run ends.
## How this is checked
| Rule | Checked by |
|---|---|
| A run checks out exactly the named branches, and reports the commits it tested | the module's tests over a forge fixture, and each run's answer |
| A run answers at once, and its state and log follow it to the end | by hand, the first run |
| The module opens no port | the composed filter of the lab's machine |
+2
View File
@@ -180,6 +180,8 @@ python3 00-META/checks/index.py fail if stale
- **0167** — [A membership carries what its module receives, and who the mesh is](0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)
- **0168** — [A converged machine is filtered by the mesh alone, and the host says what else refuses](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)
- **0169** — [A machine joins through the tunnel, and the bus is never public](0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md)
- **0170** — [The firewall seat serves its verbs, and a foreign rule set is removed through one of them](0170-the-firewall-seat-serves-its-verbs.md)
- **0172** — [The lab is a module, and runs a bed when the mesh asks](0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md)
### Its tiers, from the bottom up
+22 -3
View File
@@ -1,9 +1,10 @@
---
layer: to-be
status: in-progress
code: [mesh-lab]
updated: 2026-09-11
code: [mesh-lab, mesh-catalog modules/lab]
updated: 2026-10-02
decisions:
- 02-DECISIONS/0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0010-delivery.md
---
@@ -119,7 +120,25 @@ In order, on a machine with nothing:
6. **Verification**, as above, before anything is raised.
## Open
## The lab answers the mesh
*2026-10-02* ([ADR 0172](../../02-DECISIONS/0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md)).
Once installed, the lab is also a module: `lab`, assigned to the machine that passed `check`. Its
tools run there and nowhere else:
| tool | does |
|---|---|
| `lab_check` | the lab's `check`, on this machine |
| `lab_run` | fresh checkouts of the named branches from the forge, side by side, then the suite on the named beds; answers with an id |
| `lab_status` | where a run is, and how it ended: the commits it tested, passed and failed |
| `lab_log` | the run's output so far |
| `lab_stop` | ends a run |
The runtime is a container holding the toolchain the suite builds with. It reaches the
virtualisation daemon and the container runtime through their sockets on the machine, so what it
raises is what a hand run raises. The prerequisites above stay installed by hand. The module uses
them, and never installs them.
- **Whether the lab's bootstrap may install packages at all**, given that the mesh's rules
forbid installing by hand. The resolution is probably that the lab's bootstrap *is* the
+6
View File
@@ -9,6 +9,7 @@ code:
- mesh-host internal/apply (the service that reflects a rule set)
updated: 2026-10-02
decisions:
- 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
- 02-DECISIONS/0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md
- 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
- 02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md
@@ -766,6 +767,11 @@ tests over a fixture report check the recording, the preview's fates, the status
predicate. Live: the home server's record names the predecessor's chain as *other* and `status`
names the machine until the chain is removed by hand.
*2026-10-02, [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md):* removing what
the host reports as *other* is reached through the packet filter seat's `remove` verb, an operator's act
by name on the bus; the seat also serves `rules` and `reload`, and its holder's runtime declares the
`NET_ADMIN` capability on the machine's network. See design 33.
## 5 — Certificates
**Two authorities, kept separate on purpose.**
@@ -2,8 +2,9 @@
layer: to-be
status: implemented
code: [mesh-controller, mesh-tools]
updated: 2026-10-01
updated: 2026-10-02
decisions:
- 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
- 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
@@ -169,6 +170,18 @@ 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.
## The firewall seat's verbs, 2026-10-02
[ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md). The first node-scoped seat
to carry verbs: `node-packet-filter` serves `rules` (the filter as the machine enforces it, nftables
and legacy), `reload` (the mesh's own filter from its file) and `remove` (one rule set the mesh did
not write, named as the host reports it under ADR 0168; refusing the mesh's tables, the runtime's
own chains, a built-in chain and an active found firewall's). Every holder serves all three; the
nftables module does so from a runtime on the machine's network with `NET_ADMIN`, which is the first
container to declare a capability. Removing a predecessor's rule set is an operator's act reached
through the seat, recorded on the bus, instead of a shell on the machine. *How it is checked:* ADR
0169's table.
## 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
@@ -0,0 +1,35 @@
---
status: resolved
opened: 2026-10-02
located-in: [mesh-tools src/client.ts (toolsOn left node-scoped seats out of the listing), mesh-tools src/mcp.ts (the call resolved the key against that listing)]
fixed-by: mesh-tools 27 — node-scoped seats are listed with their scope, the verb requires the machine, and the call carries it in the subject
amended-design:
---
# 199 — A node-scoped seat's verb could not be called through the console
## What was observed
2026-10-02, the first time a node-scoped seat declared verbs
([ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md)). The packet filter's
holder on every machine served `rules`, `reload` and `remove` on the seat's per-machine subjects, and
the bus admitted them. The console answered every call with *nothing serves
node-packet-filter.rules@<machine>*.
[Design 33 §4](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) says a node-scoped seat's
tool carries the node it is asked of, as `<seat>.<verb>@<node>`. The console's listing left
node-scoped seats out — the comment said they *wait for a caller naming the node* — but the roles map
the console resolves a name against is built from that same listing. So the name never resolved as a
seat's verb, fell through to a module's subject nobody served, and the refusal named the wrong cause.
## Why it matters beyond this instance
A stated behaviour that did not happen, with a refusal that pointed elsewhere: the seat's verbs were
live on four machines and unreachable from the one surface a person uses. It could only be found by a
node-scoped seat declaring verbs, which none had.
## Resolved, 2026-10-02
mesh-tools 27: node-scoped seats are listed with their scope, their verbs take a required `node`, the
call carries it in the subject, and a call without one is refused in words. Tested with a round trip
asking one machine's holder and being refused without a machine.
@@ -0,0 +1,36 @@
---
status: open
opened: 2026-10-02
located-in: []
fixed-by:
amended-design:
---
# 200 — The controller's answer to a long console call is refused by the bus
## What was observed
2026-10-02. A `push` of the control node asked through the console came back as *mesh-controller.push
did not answer in time. Something is serving it, so this is the tool being slow rather than absent.*
The push had run; the machine applied. The bus's log on the control node, at the same moment:
```
[ERR] 10.10.0.1:56030 - cid:4015 - Publish Violation - User "controller",
Subject "_INBOX.shanks.mesh-console.WRNO5V9IJ1FX35AU5NRBEB.WRNO5V9IJ1FX35AU5PN1UW"
```
The controller's reply to the console's request was refused: the controller's bus account may not
publish to the console's reply inbox. Shorter calls (`status`, `node`, `nodes`) answer; the long ones
(`push` of a large machine, `issue`) time out on the console's side although they succeed.
## Why it matters beyond this instance
A call that succeeds and is reported as a timeout sends a person to retry what already happened — a
second push, a second issue — and reads as the mesh being slow when it is the mesh refusing itself.
Whether the inbox prefix the console uses is the one the controller's account is allowed to answer, or
the request outlives the inbox subscription, is what a diagnosis has to tell apart.
## Open questions
- Which reply inboxes may the controller's account publish to, and which does the console request on?
- Does a reply after the requester's timeout count as a violation, or is the prefix itself refused?