HQ: the as-is base layer, the process, and the names #1
@@ -40,6 +40,17 @@ look unsolvable from inside that frame:
|
|||||||
Designing from nothing is a way to find out which parts of the current shape are requirements
|
Designing from nothing is a way to find out which parts of the current shape are requirements
|
||||||
and which are residue.
|
and which are residue.
|
||||||
|
|
||||||
|
**And there is more residue than expected, from a knowable source.** This began as a
|
||||||
|
**dotfiles repository** — the first two days of history adopt dotfiles, add per-node dotfile
|
||||||
|
overrides, and introduce service symlinking with an ignore file. The flat one-directory-per-tool
|
||||||
|
catalogue, linking rather than copying, adoption of already-configured machines, per-node
|
||||||
|
overrides, and the desktop modules are all inherited from that, not chosen for a mesh. Recorded
|
||||||
|
in [`03-DESIGN/00-as-is/10-module-catalogue.md`](../../03-DESIGN/00-as-is/10-module-catalogue.md).
|
||||||
|
|
||||||
|
That makes this effort's question sharper than "what would we do differently": much of what
|
||||||
|
looks like design is a generalisation of *place files on my machines*, never revisited because
|
||||||
|
it was never stated as an assumption.
|
||||||
|
|
||||||
## The requirements this is designed against
|
## The requirements this is designed against
|
||||||
|
|
||||||
Stated by the operator, recorded here so the skeleton can be checked against them rather than
|
Stated by the operator, recorded here so the skeleton can be checked against them rather than
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ A decision procedure, so placement is answerable rather than argued. Ask in orde
|
|||||||
wins:
|
wins:
|
||||||
|
|
||||||
1. **Does it apply state on a machine?** → tier 0, inside the host.
|
1. **Does it apply state on a machine?** → tier 0, inside the host.
|
||||||
2. **Can the control plane exist without it?** If *no* → tier 1, substrate.
|
2. **Can the control plane *start* without it?** If *no* → tier 1, substrate. Note the verb:
|
||||||
|
*start*, not *function fully*. A capability the control plane loses without something is not
|
||||||
|
the same as a thing it cannot come up without — see "becoming self-hosting" below.
|
||||||
3. **Does it decide what should be true across nodes?** → tier 2, a control-plane context.
|
3. **Does it decide what should be true across nodes?** → tier 2, a control-plane context.
|
||||||
4. **Is it a way to talk to tier 2, holding no logic of its own?** → tier 3, a surface.
|
4. **Is it a way to talk to tier 2, holding no logic of its own?** → tier 3, a surface.
|
||||||
5. **Otherwise** → tier 4, a workload.
|
5. **Otherwise** → tier 4, a workload.
|
||||||
@@ -50,13 +52,102 @@ inverse — substrate reaching into the catalogue for a module — would not be,
|
|||||||
the naive "twice" answer would have created.
|
the naive "twice" answer would have created.
|
||||||
|
|
||||||
The same reasoning places the rest of the substrate: the bus, the object store, the image
|
The same reasoning places the rest of the substrate: the bus, the object store, the image
|
||||||
registry. Each fails step 2, each lives once, each is pinned.
|
registry. Each fails step 2, each lives once, each is pinned. That is the whole substrate: four
|
||||||
|
services, and the deliberate absence of a fifth.
|
||||||
|
|
||||||
**One genuine boundary case, flagged rather than decided.** The identity provider passes step 2
|
**The identity provider was raised as a boundary case and is settled: it is not substrate.**
|
||||||
only if the control plane delegates authentication rather than doing it natively. If tier 2
|
The mesh does not require one — tier 2 authenticates its own callers natively, and an identity
|
||||||
authenticates callers itself, the identity provider drops to tier 4 and the substrate has four
|
provider is a service the mesh hosts like any other. The substrate is four services, not five.
|
||||||
services instead of five. The test does not answer this; it turns it into a question with a
|
|
||||||
clear shape, which is what a test is for.
|
The test earned its keep here by turning a vague unease into one answerable question — *does the
|
||||||
|
control plane delegate authentication?* — rather than a debate about how important identity
|
||||||
|
feels.
|
||||||
|
|
||||||
|
## Becoming self-hosting — the forge and the registries
|
||||||
|
|
||||||
|
Self-improvement means the mesh hosts the things it improves itself with: a forge, an image
|
||||||
|
registry, a package registry. The obvious worry is that these duplicate — a `hal-mesh-gitea`
|
||||||
|
for the mesh and a `gitea` for everyone else. **They do not, and the reason is worth stating
|
||||||
|
carefully, because it is the same reason the bootstrap keeps failing today.**
|
||||||
|
|
||||||
|
### They are not substrate
|
||||||
|
|
||||||
|
Run the test with the sharpened verb. *Can the control plane start without a forge?* **Yes.** It
|
||||||
|
comes up, holds inventory, answers questions and manages nodes with the modules it already has.
|
||||||
|
What it cannot do is **change itself**. That is a capability, not a precondition.
|
||||||
|
|
||||||
|
So: forge, image registry and package registry are **tier 4 workloads**. One module each, in the
|
||||||
|
catalogue, exactly like the media server. There is no mesh-specific copy.
|
||||||
|
|
||||||
|
This is not a technicality. It buys a property worth having: **if the forge dies, the mesh keeps
|
||||||
|
running.** Nodes stay managed, services stay up, only self-modification stops. Putting the forge
|
||||||
|
in the substrate would make losing it fatal, for no gain.
|
||||||
|
|
||||||
|
### But delivery needs them — is that not an upward dependency?
|
||||||
|
|
||||||
|
It would be, stated naively, and that would break the one rule the whole skeleton rests on.
|
||||||
|
|
||||||
|
It is resolved the way the constitution already says to resolve it — **depend on abstractions,
|
||||||
|
not on concrete dependencies**. Tier 2's delivery context does not require *the forge module*.
|
||||||
|
It declares requirements:
|
||||||
|
|
||||||
|
| Delivery requires | Satisfied by |
|
||||||
|
|---|---|
|
||||||
|
| a source of record for module code | whichever module provides it |
|
||||||
|
| somewhere to publish images | whichever module provides it |
|
||||||
|
| somewhere to publish packages | whichever module provides it |
|
||||||
|
| somewhere to put build artifacts | the substrate's object store |
|
||||||
|
|
||||||
|
Tier 2 defines the requirement; tier 4 provides the implementation; the binding is data. The
|
||||||
|
dependency points **downward from the provider to the interface**, which is legal, and the
|
||||||
|
control plane never names a concrete module.
|
||||||
|
|
||||||
|
The mechanism for this already exists and is the mesh's most valuable one: **provisioning**. A
|
||||||
|
module declares what it provides; a consumer declares what it requires; the mesh binds them.
|
||||||
|
The only new idea is that **the control plane is itself a consumer** — it has requirements, and
|
||||||
|
they are satisfied the same way a workload's are.
|
||||||
|
|
||||||
|
That generalisation is significant enough to need its own study, and is not settled here.
|
||||||
|
|
||||||
|
### Self-hosting is a state the mesh reaches, not a precondition
|
||||||
|
|
||||||
|
This is the part today's mesh gets wrong, and it explains a recurring class of pain.
|
||||||
|
|
||||||
|
A first node comes up from **pinned external artifacts** — upstream images, by digest, carried
|
||||||
|
in the bundle. It has to: the mesh's own registry does not exist yet, and cannot. The mesh at
|
||||||
|
this point is running and manages nodes, and is not yet self-hosting.
|
||||||
|
|
||||||
|
Self-hosting is then **reached**: the forge is installed as an ordinary workload, the mesh's own
|
||||||
|
source moves into it, the registries come up, and delivery's requirements are re-bound from
|
||||||
|
external providers to internal ones. From that point the mesh builds and deploys itself.
|
||||||
|
|
||||||
|
Stated as a lifecycle:
|
||||||
|
|
||||||
|
```
|
||||||
|
pinned external artifacts ─► mesh runs, manages nodes
|
||||||
|
│
|
||||||
|
│ forge + registries installed as workloads
|
||||||
|
│ delivery's requirements re-bound
|
||||||
|
▼
|
||||||
|
mesh builds and deploys itself
|
||||||
|
```
|
||||||
|
|
||||||
|
**Today's mesh assumes the second state from the first moment.** Its source, its packages and
|
||||||
|
its images are all expected to be self-hosted before there is anything to host them — which is
|
||||||
|
why raising a first node needs a script that exists solely to paper over the impossibility, and
|
||||||
|
why that script is the least-exercised path in the system.
|
||||||
|
|
||||||
|
Making the transition explicit has a second benefit: it is reversible. A mesh whose forge is
|
||||||
|
broken can re-bind delivery to external providers and keep improving itself while it repairs
|
||||||
|
the forge. Today that escape hatch does not exist, because the dependency is not expressed
|
||||||
|
anywhere it could be changed.
|
||||||
|
|
||||||
|
### So, concretely
|
||||||
|
|
||||||
|
One `gitea` module. One image-registry module. One package-registry module. Each a tier-4
|
||||||
|
workload. The mesh's own instances are distinguished from any other instance **by what they are
|
||||||
|
bound to, not by being different modules** — precisely the same answer as postgres, arrived at
|
||||||
|
by the same test.
|
||||||
|
|
||||||
## The five fates of a module
|
## The five fates of a module
|
||||||
|
|
||||||
@@ -145,7 +236,6 @@ hal-substrate/ TIER 1
|
|||||||
bus/<broker>/
|
bus/<broker>/
|
||||||
objects/<object-store>/
|
objects/<object-store>/
|
||||||
images/<registry>/
|
images/<registry>/
|
||||||
identity/<idp>/ boundary case — see above
|
|
||||||
|
|
||||||
hal-mesh/ TIER 2
|
hal-mesh/ TIER 2
|
||||||
record/ the event log contexts integrate through
|
record/ the event log contexts integrate through
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
status: active
|
||||||
|
initiated: 2026-08-23
|
||||||
|
touches:
|
||||||
|
- 03-DESIGN/00-as-is/03-provisioning.md
|
||||||
|
- 02-DECISIONS/0005-capabilities-are-provisioned-on-declaration.md
|
||||||
|
- 01-RESEARCH/006-mesh-from-scratch/code-skeleton.md
|
||||||
|
became: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# 007 — Provisioning as the mesh's core mechanism
|
||||||
|
|
||||||
|
## What is being investigated
|
||||||
|
|
||||||
|
Provisioning is the mechanism the whole mesh rests on: a module declares what it needs, and the
|
||||||
|
mesh makes it exist, generates the credential, records the grant, and puts the values where the
|
||||||
|
module will read them. [ADR 0005](../../02-DECISIONS/0005-capabilities-are-provisioned-on-declaration.md)
|
||||||
|
calls it the mesh's core concern rather than its plumbing.
|
||||||
|
|
||||||
|
[Research 006](../006-mesh-from-scratch/code-skeleton.md) then asks it to carry **more**: the
|
||||||
|
control plane becomes a consumer with its own requirements — a source of record, an image
|
||||||
|
registry, a package registry — satisfied by the same mechanism. That generalisation is only
|
||||||
|
safe if the mechanism is sound, and the as-is record says it is not, in named ways.
|
||||||
|
|
||||||
|
## Why now
|
||||||
|
|
||||||
|
Four weaknesses are already documented in
|
||||||
|
[`03-DESIGN/00-as-is/03-provisioning.md`](../../03-DESIGN/00-as-is/03-provisioning.md), each
|
||||||
|
observed rather than theorised:
|
||||||
|
|
||||||
|
- **Rotation has no fan-out.** A shared credential can be rotated without telling the peers
|
||||||
|
holding the old one. This has locked the mesh out of its own broker.
|
||||||
|
- **A grant is not a check.** The record says a resource was provisioned. Nothing verifies it
|
||||||
|
still exists, still has that credential, or is reachable from where the consumer runs.
|
||||||
|
- **A frozen password outlives its generation.** A generated secret written once diverges from a
|
||||||
|
persistent data directory initialised earlier, and presents as an authentication error.
|
||||||
|
- **No requirements is indistinguishable from provisioning that did not run.** A module that
|
||||||
|
declares nothing skips the stage, which is correct, and looks identical to failure.
|
||||||
|
|
||||||
|
Generalising a mechanism with these properties to the control plane's own dependencies would
|
||||||
|
make each of them fatal rather than annoying.
|
||||||
|
|
||||||
|
## The questions
|
||||||
|
|
||||||
|
| Question | Why it matters |
|
||||||
|
|---|---|
|
||||||
|
| What does a **grant** mean, exactly — a record that a resource was created, or a claim about the world that is continuously reconciled? | The difference between the current model and one where "provisioned" is checkable. Almost every weakness above is a symptom of the first answer. |
|
||||||
|
| How is a credential **rotated** with fan-out to every holder? | The mechanism grants easily and regrants not at all. This is the most damaging gap and it has taken the mesh down. |
|
||||||
|
| Can the **control plane** hold requirements, and what satisfies them before anything is installed? | The generalisation research 006 needs. Ties directly to the self-hosting transition. |
|
||||||
|
| What happens when a requirement **cannot** be satisfied — no provider, provider on an unreachable node, provider not yet installed? | Today this is silent or a stall. It should be a stated, visible state. |
|
||||||
|
| Does a requirement belong to a **module** or to one of its **parts**? | Research 006 splits `feature` into artifact and part. A part-scoped requirement means a database is not provisioned where the part that needs it is not installed. |
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
status: active
|
||||||
|
initiated: 2026-08-23
|
||||||
|
touches:
|
||||||
|
- 03-DESIGN/00-as-is/04-delivery.md
|
||||||
|
- 02-DECISIONS/0014-build-publish-and-deploy-are-three-silos.md
|
||||||
|
- 02-DECISIONS/0013-an-artifact-is-build-output.md
|
||||||
|
- 01-RESEARCH/006-mesh-from-scratch/code-skeleton.md
|
||||||
|
became: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# 008 — The coordinator: a change checked in becomes a deployed state
|
||||||
|
|
||||||
|
## What is being investigated
|
||||||
|
|
||||||
|
The mesh's own continuous delivery: a change is committed, and the mesh ends up in the state
|
||||||
|
that change describes — across every node the change touches, with a verdict that says whether
|
||||||
|
it worked.
|
||||||
|
|
||||||
|
The coordinator is what orchestrates that, and it is the mesh's most consequential machinery:
|
||||||
|
everything reaches every node through it.
|
||||||
|
|
||||||
|
## Why now
|
||||||
|
|
||||||
|
The as-is record ([`03-DESIGN/00-as-is/04-delivery.md`](../../03-DESIGN/00-as-is/04-delivery.md))
|
||||||
|
names problems that are structural rather than incidental:
|
||||||
|
|
||||||
|
- **A green pipeline proves transport, not effect.** The stages report that a message was
|
||||||
|
dispatched and accepted, which is not the same as the thing running, correct, or present.
|
||||||
|
This is the mesh's single most consistent failure shape.
|
||||||
|
- **Detection is the most fragile input.** A merge that creates no pipeline, with nothing saying
|
||||||
|
so, is the characteristic bad outcome — and it has happened for reasons unrelated to the
|
||||||
|
change.
|
||||||
|
- **The fan-out point is asymmetric.** The build node has already passed two silos when work
|
||||||
|
fans out, and code that knew only about the first parked it forever while every other node
|
||||||
|
deployed cleanly.
|
||||||
|
- **There is no end-to-end coverage.** The harness has not built since 2026-06-04
|
||||||
|
([`04-ISSUES/005`](../../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md)).
|
||||||
|
|
||||||
|
Research 006 adds a requirement the current design does not have: the coordinator must work
|
||||||
|
**before the mesh is self-hosting**, when source and artifacts come from outside, and keep
|
||||||
|
working across the transition to self-hosted providers.
|
||||||
|
|
||||||
|
## The questions
|
||||||
|
|
||||||
|
| Question | Why it matters |
|
||||||
|
|---|---|
|
||||||
|
| What is a **deployed state**, and how does the mesh know it is in one? | Everything follows from this. If a stage reports transport, "deployed" is a claim nobody checked. A desired-state model with reconciliation gives a different answer from a job-completion model. |
|
||||||
|
| Does the coordinator dispatch **stages**, or converge nodes on a **declaration**? | The current model is a state machine over stages. The alternative is that a node is told what should be true and reports what is. The second makes drift visible; the first cannot see it. |
|
||||||
|
| How does a change **become** a pipeline, reliably? | Detection has failed for reasons unrelated to the change, silently. |
|
||||||
|
| What produces a **verdict**, and what is it a verdict about? | Ties to the lab ([ADR 0016](../../02-DECISIONS/0016-a-lab-node-is-a-virtual-machine.md)) and to a module carrying its own assertions. |
|
||||||
|
| How does delivery work **before self-hosting**, and across the transition? | From research 006: source and artifacts start external and are re-bound to internal providers. The coordinator has to be indifferent to which. |
|
||||||
|
| Does the **three-silo** split survive the artifact/part split? | [ADR 0014](../../02-DECISIONS/0014-build-publish-and-deploy-are-three-silos.md) is cardinality-driven, and research 006 renames the thing the cardinality is about. |
|
||||||
@@ -62,6 +62,42 @@ addressed in principle by
|
|||||||
[ADR 0017](../../02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md), which
|
[ADR 0017](../../02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md), which
|
||||||
deliberately does not yet settle the domain list.
|
deliberately does not yet settle the domain list.
|
||||||
|
|
||||||
|
## Where the shape came from
|
||||||
|
|
||||||
|
The catalogue's shape is not arbitrary and it is not a series of mistakes. **This repository
|
||||||
|
began as a dotfiles repository**, and most of what looks inexplicable is inherited from that
|
||||||
|
directly.
|
||||||
|
|
||||||
|
The evidence is in the first two days of history: *adopt desktop dotfiles and scripts from the
|
||||||
|
original dotfiles repository*; *per-node dotfile overrides*; *service symlinking, node
|
||||||
|
`.dfignore`, headless server support* — all on 2026-02-24 and 2026-02-25, before anything
|
||||||
|
resembling a mesh existed. Forty-five commits mention dotfiles.
|
||||||
|
|
||||||
|
Read that way, several things stop being puzzles:
|
||||||
|
|
||||||
|
| Feature of today's shape | Dotfiles ancestor |
|
||||||
|
|---|---|
|
||||||
|
| One flat directory per piece of software | Exactly how a dotfiles repository is organised |
|
||||||
|
| **Linking rather than copying** as a stated design principle | How every dotfiles manager works — the whole category is built on it |
|
||||||
|
| **Adoption** — taking over a machine that already exists, with its own configuration | The core dotfiles verb, and the reason a machine could be brought in at all |
|
||||||
|
| **Per-node overrides** | Per-host dotfile overrides, generalised into per-node module settings |
|
||||||
|
| Desktop and workstation modules — browser, file manager, editors, session, audio, personal scripts | Dotfiles content that became "modules" when modules became the only unit |
|
||||||
|
| An ignore file controlling what is placed on a machine | A dotfiles manager's ignore file |
|
||||||
|
|
||||||
|
The generalisation from *place files on my machines* to *manage a mesh of nodes* was the right
|
||||||
|
move and it worked. What it did not do is revisit the assumptions underneath, because they were
|
||||||
|
never stated as assumptions — they were just how the thing already worked.
|
||||||
|
|
||||||
|
**This is the most useful single fact for anyone changing the catalogue**, and it is why the
|
||||||
|
linking principle in particular reads as a deliberate architectural choice when it is an
|
||||||
|
inheritance. See [ADR 0018](../../02-DECISIONS/0018-the-mesh-creates-no-symlinks.md), whose case
|
||||||
|
this strengthens: the argument for links was never made *for a mesh*.
|
||||||
|
|
||||||
|
It also explains the measurement in
|
||||||
|
[research 005](../../01-RESEARCH/005-domain-grouping/analysis.md). Fifty modules that never
|
||||||
|
change alongside anything are not fifty missing domains — many are dotfiles-era entries for one
|
||||||
|
piece of software, which never shared a domain because they never had one.
|
||||||
|
|
||||||
## Two properties worth keeping
|
## Two properties worth keeping
|
||||||
|
|
||||||
Whatever replaces the shape, two things about it are right.
|
Whatever replaces the shape, two things about it are right.
|
||||||
|
|||||||
Reference in New Issue
Block a user