skeleton: hal-agent -> hal-host, and the two gaps the questions found

Agent is a first-class concept here — a participant, some of whom are
human, holding identity and memory. Using it for the tier-0 node binary
put both meanings in one document: hal-agent at tier 0, agents at tier 2.
That is the anatomy-naming failure again, an evocative domain word
pointing at infrastructure, and how-we-build 4 exists to catch it.

ADR 0015's own title supplies the fix: the mesh brokers, nodes host,
agents think. So the control plane brokers (hal-mesh), the tier-0 binary
hosts (hal-host), the participant thinks (agents, untouched). hal-node was
rejected — Node is the inventory aggregate, the binary is what runs on it.
Recorded in the document as a near-miss rather than quietly corrected.

Tiers now explained before the tree, as a boot narrative, with the point
they were carrying made explicit: dependencies point only downward, that
is the whole bootstrap answer, and the current mesh violates it — the
database is a module, modules come from the pipeline, the pipeline needs
the database.

Connectivity gains the part that was missing. Naming a context says who
decides, not who runs it: overlay membership is tier 0 in the host, policy
is tier 2, machinery is tier 4 modules. And the host's link to the control
plane deliberately does not run over the overlay, or the overlay would
have to exist before a node could be told how to join it.

'tier' is this document's coinage and did not land on first reading; that
is now an open question rather than settled vocabulary.
This commit is contained in:
2026-08-23 20:32:20 +02:00
parent b4365d8aa4
commit 00b8398d07
2 changed files with 86 additions and 7 deletions
@@ -77,3 +77,4 @@ the catalogue where modules genuinely change together under one intent. The skel
| One repository per tier, or per context? | Already open from ADR 0015 as "catalogue destination — one repository or many". The skeleton assumes per tier and does not settle it. | | One repository per tier, or per context? | Already open from ADR 0015 as "catalogue destination — one repository or many". The skeleton assumes per tier and does not settle it. |
| Does an unprivileged node earn a place in the inventory, or only a presence? | Decides whether "node" means one thing or two. | | Does an unprivileged node earn a place in the inventory, or only a presence? | Decides whether "node" means one thing or two. |
| Does `feature` survive? | The skeleton splits it in two and argues the conflation is what makes the delivery pipeline hard to reason about. Unproven. | | Does `feature` survive? | The skeleton splits it in two and argues the conflation is what makes the delivery pipeline hard to reason about. Unproven. |
| Is `tier` the right word? | It is this document's coinage, not established vocabulary, and it did not land on first reading. `boot order` and `ring` are the alternatives. The concept is settled; the word is not. |
+85 -7
View File
@@ -8,10 +8,45 @@ updated: 2026-08-23
Repositories at the root, modules inside them, parts at the leaf. Four tiers, and a dependency Repositories at the root, modules inside them, parts at the leaf. Four tiers, and a dependency
rule that only points downward. rule that only points downward.
## What a tier is
A tier answers one question: **what has to exist before this can exist?** It is not importance,
and it is not a layer in the networking sense. It is bootstrap order, made explicit.
Walk a bare machine to a running mesh and the tiers fall out of the story:
```
bare machine
│ one command lands ONE binary. Nothing else exists. TIER 0 host
│
│ it reads a pinned file it already carries and raises a
│ database, a bus, an object store, a registry, an
│ identity provider — locally, alone. TIER 1 substrate
│
│ on those, the mesh's brain starts: which nodes exist,
│ what runs where, what is reachable. TIER 2 control plane
│
│ ways to talk to that brain. TIER 3 surfaces
│
└ everything the mesh then carries. TIER 4 workloads
```
**Dependencies point only downward.** The substrate never references the control plane. That
one constraint is the entire bootstrap answer, because it guarantees there is always a place to
start.
Why it earns its keep here: **the current mesh violates this, and that is the circularity that
keeps recurring.** The mesh database is a module; modules are installed by the delivery
pipeline; the pipeline needs the database. No order works, so a first-node script exists to
paper over it, and every later substrate change has to pretend the problem is not there.
A tier is not a repository and not a bounded context. Those are different cuts: a context says
*who owns this concept*, a tier says *what must already be running*.
## The tree ## The tree
``` ```
hal-agent/ TIER 0 — the only thing ever installed by hand hal-host/ TIER 0 — the only thing ever installed by hand
apply/ reconcile declared state on this machine apply/ reconcile declared state on this machine
inventory/ what this node is, has, and is capable of inventory/ what this node is, has, and is capable of
link/ the single outbound connection to the control plane link/ the single outbound connection to the control plane
@@ -56,7 +91,7 @@ hal-hq/ this repository
## The dependency rule ## The dependency rule
**A tier may depend only on tiers below it.** Substrate never references the control plane. **A tier may depend only on tiers below it.** Substrate never references the control plane.
The control plane never reaches into a node except through the agent. A surface holds no logic The control plane never reaches into a node except through the host. A surface holds no logic
a second surface would have to reimplement. a second surface would have to reimplement.
This is the whole of the bootstrap answer, and per this repository's own rule it must say how This is the whole of the bootstrap answer, and per this repository's own rule it must say how
@@ -72,9 +107,9 @@ needs the database and the bus. The first node is therefore raised by a special
exists only because of the circularity, and every later change to the substrate has to pretend exists only because of the circularity, and every later change to the substrate has to pretend
the circularity is not there. the circularity is not there.
**The move.** The agent can apply a declaration without anyone telling it to. The substrate is **The move.** The host can apply a declaration without anyone telling it to. The substrate is
a **pinned bundle** the agent carries: a fixed, versioned, self-contained descriptor of the a **pinned bundle** the host carries: a fixed, versioned, self-contained descriptor of the
five services and nothing else. Raising a first node is `agent apply substrate.lock` — not a five services and nothing else. Raising a first node is `host apply substrate.lock` — not a
special path, just the ordinary one with no control plane on the other end. special path, just the ordinary one with no control plane on the other end.
The circularity disappears rather than being worked around: **the substrate is applied by tier The circularity disappears rather than being worked around: **the substrate is applied by tier
@@ -85,13 +120,13 @@ re-applying, not by the pipeline. It gets less machinery than everything else
selection, no provisioning, no fan-out — and that is the point. Five services justify a selection, no provisioning, no fan-out — and that is the point. Five services justify a
simpler mechanism than a hundred. simpler mechanism than a hundred.
## Move 2 — the agent is one binary with capability profiles ## Move 2 — the host is one binary with capability profiles
**The problem.** Everything assumes root on a machine whose packages, services and network the **The problem.** Everything assumes root on a machine whose packages, services and network the
mesh owns. A phone cannot offer that, and neither can a work laptop. The current answer would mesh owns. A phone cannot offer that, and neither can a work laptop. The current answer would
be a lightweight fork, which means two implementations and one of them rotting. be a lightweight fork, which means two implementations and one of them rotting.
**The move.** One agent, one binary, and a **profile** it detects rather than is told: **The move.** One host binary, and a **profile** it detects rather than is told:
| Profile | Can | Typical | | Profile | Can | Typical |
|---|---|---| |---|---|---|
@@ -131,6 +166,29 @@ belongs in a new record that extends ADR 0015 the way
[ADR 0017](../../02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md) does — [ADR 0017](../../02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md) does —
not written here. not written here.
### Where the networking actually lives
A context is an authority, not a running thing, so naming one does not say who brings the
overlay up. That splits three ways, and the split is the design:
| Concern | Tier | Why there |
|---|---|---|
| **Overlay membership** — this node joins, holds an address, keeps the tunnel up | **0, in the host** | Everything cross-node needs it *before* the substrate is reachable from elsewhere. A module cannot provide it, because installing a module is itself a cross-node operation. |
| **Policy** — who holds which address, what resolves, what is exposed, what is filtered | **2, `connectivity`** | Bookkeeping and authority. It decides; it runs nothing. |
| **Machinery** — resolver, reverse proxy, firewall backend, certificate issuance | **4, modules** | Swappable, and not every node needs them. A node without a reverse proxy is still a node. |
There is a second circularity hiding here, and it has to be closed explicitly: **the host's link
to the control plane does not run over the overlay.** If it did, the overlay would have to be up
before the host could be told how to join it. The link is ordinary outbound internet to a public
endpoint; the overlay carries node-to-node traffic only.
Joining is therefore: host lands → links out with a join token → control plane returns an
address and keys → host raises membership → the substrate on other nodes becomes reachable.
This also makes the `edge` profile honest rather than special-cased. A phone can hold overlay
membership in userspace without privilege, and cannot run the machinery. That is the profile
distinction doing its job.
## Move 4 — `feature` splits in two ## Move 4 — `feature` splits in two
The invitation was to check whether the concept survives. It does not, in one piece. The invitation was to check whether the concept survives. It does not, in one piece.
@@ -200,3 +258,23 @@ being the one thing nobody exercises until it breaks.
is one concept or two. is one concept or two.
- The migration. Nothing here says how today's mesh becomes this, and the skeleton is worth - The migration. Nothing here says how today's mesh becomes this, and the skeleton is worth
little until that is costed. little until that is costed.
## A naming near-miss, recorded
The tier-0 binary was first called `hal-agent`, because "node agent" is the reflex everywhere
else in the industry. That is wrong here, and wrong in the specific way
[`how-we-build.md`](../../00-META/how-we-build.md) §4 exists to catch: **Agent** is a
first-class concept in this mesh — a participant, some of whom are human, holding identity and
memory ([ADR 0012](../../02-DECISIONS/0012-agents-are-persistent-employees.md)). One document
carried both meanings.
It is the same failure as the anatomy naming in the current runtime: an evocative domain word
pointing at infrastructure.
[ADR 0015](../../02-DECISIONS/0015-mesh-brokers-nodes-host-agents-think.md) supplies the fix in
its own title — *the mesh brokers capabilities; nodes host; agents think.* Three verbs, three
components: the control plane **brokers** (`hal-mesh`), the tier-0 binary **hosts**
(`hal-host`), the participant **thinks** (`agents`, untouched).
`hal-node` was the alternative and was rejected: *Node* is the aggregate in the inventory — the
record of a machine — while the binary is what runs on it and does the hosting.