diff --git a/00-META/how-we-build.md b/00-META/how-we-build.md index 94650de..9926fd4 100644 --- a/00-META/how-we-build.md +++ b/00-META/how-we-build.md @@ -40,7 +40,7 @@ incident behind it is not written down, and the fix is to write it down, not to | **Every schema change is a migration** | Numbered, in the module's own language, compiled with it. Both a baseline for a fresh installation *and* an incremental migration for installations that already exist. If code references a column, the migration creating it must exist. [ADR 0006](../02-DECISIONS/0006-schema-changes-are-numbered-migrations.md) | | **Never bypass the pipeline** | No manual database edit, no manual restart as a workaround. Fix the cause and deploy. A workaround that works is a workaround that is never removed, and the next person cannot tell the node from its declaration. | | **Never create a symlink** | A hand-made link caused production data loss through container volume resolution, and the judgement needed to make a safe exception is exactly the judgement unavailable at the moment it matters. Today the installer owns and reconciles the links the mesh still uses [ADR 0011](../02-DECISIONS/0011-the-installer-owns-linking.md); the intent is that the mesh creates none at all [ADR 0018](../02-DECISIONS/0018-the-mesh-creates-no-symlinks.md). Neither reading permits you to make one. | -| **Never push directly to the main branch** | Branch, push, review, merge. Every merge is a human checkpoint, without exception. | +| **Never push directly to the main branch** | Branch, push, review, merge. Every merge is a human checkpoint, without exception — **including in this repository**. A documentation repository is not a lower tier of care; a decision record lands the same way a service does. | | **One change per pull request, and never merge your own** | Unrelated improvements bundled together cannot be reviewed or reverted separately. Self-merging removes the checkpoint that is the entire point. | | **Never open a pull request unprompted** | A permissions list saying it is allowed is not a request. | | **A failed step fails the job** | A sequence that continues past a failure does the next thing in the wrong place. Gate each step on the last. [ADR 0008](../02-DECISIONS/0008-a-failed-step-fails-the-job.md), and §5. | diff --git a/01-RESEARCH/006-mesh-from-scratch/00-overview.md b/01-RESEARCH/006-mesh-from-scratch/00-overview.md new file mode 100644 index 0000000..dbcbdde --- /dev/null +++ b/01-RESEARCH/006-mesh-from-scratch/00-overview.md @@ -0,0 +1,79 @@ +--- +status: active +initiated: 2026-08-23 +touches: + - 02-DECISIONS/0015-mesh-brokers-nodes-host-agents-think.md + - 02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md + - 03-DESIGN/00-as-is/00-overview.md + - 03-DESIGN/01-to-be/00-work-breakdown.md +became: [] +--- + +# 006 — The mesh designed from nothing + +## What is being investigated + +What the mesh would look like if it were laid out today, with the requirements known and none +of the accumulated shape — expressed as a **skeleton**: repositories at the root, modules +inside them, and whatever turns out to be the right leaf unit below that. + +The deliverable is [`skeleton.md`](skeleton.md). + +## Why + +Every structural decision so far has been a **correction**: eight contexts replacing thirty-three +modules ([ADR 0015](../../02-DECISIONS/0015-mesh-brokers-nodes-host-agents-think.md)), domains +replacing single-function modules +([ADR 0017](../../02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md)). A +correction inherits the frame of the thing it corrects, and two of the mesh's oldest problems +look unsolvable from inside that frame: + +- **The bootstrap circularity.** The mesh needs a database, a bus, a registry and an identity + provider. Those are modules the mesh installs. The mesh cannot install them before it exists. + This has been worked around repeatedly and never designed away. +- **Participation requires privilege.** Everything assumes root on a machine whose packages and + services the mesh owns. A phone cannot participate on those terms, and neither can a machine + someone else administers. + +Designing from nothing is a way to find out which parts of the current shape are requirements +and which are residue. + +## The requirements this is designed against + +Stated by the operator, recorded here so the skeleton can be checked against them rather than +against taste: + +1. The mesh manages multiple computers — **full control**, through modules installed to nodes. +2. Mesh state lives in a **database**: which modules on which nodes, logs, configuration. +3. Configuration has **several touchpoints** — tool surface, web interface, others — all hosted + by the mesh itself. +4. **Connectivity** is core: every node reachable from every other over a shared overlay, some + nodes publicly exposed, firewalls configured. +5. The mesh **hosts applications** — and requires some of them itself. This is the circularity. +6. **Arch Linux only for now**; ideally any device, including phones, on lighter terms. +7. The end goal is to **operate an IT company** on it — development, design, deployment, full + circle, self-hosted. Personal cloud infrastructure. +8. **Agents make it self-improving and self-healing.** +9. It is **end-to-end testable on one machine** + ([ADR 0016](../../02-DECISIONS/0016-a-lab-node-is-a-virtual-machine.md)). + +## Status + +A first skeleton exists, with four design moves that the current shape does not have. It is +`active` because two of them are unproven and one contradicts a record that is already +accepted. + +**Finding worth stating up front:** [ADR 0015](../../02-DECISIONS/0015-mesh-brokers-nodes-host-agents-think.md) +names nine bounded contexts and **none of them owns connectivity** — no overlay, no resolution, +no firewall, no ingress. Requirement 4 has no home in the accepted decomposition, while +[research 005](../005-domain-grouping/analysis.md) found reachability to be the *only* part of +the catalogue where modules genuinely change together under one intent. The skeleton adds it. + +## Open questions + +| Question | Why it is open | +|---|---| +| Does the record — the event log contexts integrate through — belong to the substrate or the control plane? | It is infrastructure by shape and domain by content. Placing it wrong reintroduces a circularity. | +| 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 `feature` survive? | The skeleton splits it in two and argues the conflation is what makes the delivery pipeline hard to reason about. Unproven. | diff --git a/01-RESEARCH/006-mesh-from-scratch/skeleton.md b/01-RESEARCH/006-mesh-from-scratch/skeleton.md new file mode 100644 index 0000000..c8895f3 --- /dev/null +++ b/01-RESEARCH/006-mesh-from-scratch/skeleton.md @@ -0,0 +1,202 @@ +--- +effort: 006-mesh-from-scratch +updated: 2026-08-23 +--- + +# The skeleton + +Repositories at the root, modules inside them, parts at the leaf. Four tiers, and a dependency +rule that only points downward. + +## The tree + +``` +hal-agent/ TIER 0 — the only thing ever installed by hand + apply/ reconcile declared state on this machine + inventory/ what this node is, has, and is capable of + link/ the single outbound connection to the control plane + store/ embedded local state — authoritative while disconnected + profile/ capability detection: managed · user · edge + substrate.lock pinned tier-1 descriptor, appliable with no mesh present + +hal-substrate/ TIER 1 — declarations only, no logic of its own + store/ relational state + bus/ commands and events + objects/ blobs and build artifacts + images/ container images + identity/ the identity provider + bundle.yml the pinned set tier 0 can raise alone + +hal-mesh/ TIER 2 — the control plane + record/ the event log every context integrates through + inventory/ nodes · modules · assignments · versions + config/ settings · secrets · derivation onto nodes + connectivity/ overlay · resolution · exposure · filtering · certificates + provisioning/ resource grants between modules + delivery/ source → artifact → node + observability/ health · logs · metrics · alerts + identity/ agents · humans · services · authorisation + work/ tasks · workflows · runs + knowledge/ memory · documents · retrieval + api/ the one interface every surface speaks to + +hal-surfaces/ TIER 3 — thin; no logic lives here + tools/ the agent-facing tool surface + web/ the operator-facing interface + cli/ the shell-facing interface + +hal-catalog/ TIER 4 — what the mesh hosts + / grouped per ADR 0017, list per research 005 + +hal-lab/ the whole mesh, disposable, on one machine +hal-sdk/ contracts shared across tiers — types, not behaviour +hal-hq/ this repository +``` + +## The dependency rule + +**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 +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 +it is checked: a dependency-direction lint in the build, failing on an upward import. A tier +rule enforced by intention is the same as no tier rule — that is +[ADR 0008](../../02-DECISIONS/0008-a-failed-step-fails-the-job.md) applied to architecture. + +## Move 1 — the substrate is applied, not delivered + +**The problem.** The mesh needs a database, a bus, an object store, a registry and an identity +provider. Today those are modules, and modules are installed by the delivery pipeline, which +needs the database and the bus. The first node is therefore raised by a special script that +exists only because of the circularity, and every later change to the substrate has to pretend +the circularity is not there. + +**The move.** The agent 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 +five services and nothing else. Raising a first node is `agent apply substrate.lock` — not a +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 +0, the mesh is delivered by tier 2, and they are different mechanisms on purpose.** + +The price is real and should be named: the substrate is upgraded by bumping a pin and +re-applying, not by the pipeline. It gets less machinery than everything else — no per-node +selection, no provisioning, no fan-out — and that is the point. Five services justify a +simpler mechanism than a hundred. + +## Move 2 — the agent is one binary with capability profiles + +**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 +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: + +| Profile | Can | Typical | +|---|---|---| +| `managed` | packages, services, network, filesystem — the full surface | a machine the mesh owns | +| `user` | user-level services and tools; no package or network management | a shared or administered machine | +| `edge` | report presence, relay, expose a tool surface; hold nothing | a phone | + +A module declares which profiles it can land on. Assignment to an incapable node fails at +declaration time, not at deploy time — a phone is not a machine that fails to install a +firewall; it is a node the firewall module cannot be assigned to. + +This makes the phone case a **capability question rather than a platform question**, which is +what keeps it from becoming a second implementation. It also removes the current unstated +assumption that every node is equivalent — already false, and today handled by remembering. + +## Move 3 — connectivity becomes a context + +[ADR 0015](../../02-DECISIONS/0015-mesh-brokers-nodes-host-agents-think.md) names nine contexts +and none of them owns the overlay, the resolver, the firewall or the ingress. `config` owns +PKI, which is the closest thing, and it is not close. + +Meanwhile [research 005](../005-domain-grouping/analysis.md) measured the whole catalogue and +found that reachability is the **only** place where modules genuinely change together under one +intent — the proxy with the resolver, the firewall with the overlay, repeatedly, because *how a +node is reachable* is one question asked in four places. + +So the evidence and the gap point the same way. `connectivity` owns: + +- the overlay every node joins, and the addresses on it +- name resolution, internal and public +- exposure — which services answer from outside, on which names +- filtering — what may reach a node at all +- certificates for both name spaces + +This is an addition to an accepted record, so it is a decision, not a drafting choice. It +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 — +not written here. + +## Move 4 — `feature` splits in two + +The invitation was to check whether the concept survives. It does not, in one piece. + +Today a **feature** means both *a thing built once* and *a thing selected per node*, and the +delivery pipeline is hard to reason about precisely because those have different cardinality +and one word ([ADR 0014](../../02-DECISIONS/0014-build-publish-and-deploy-are-three-silos.md) +is the pipeline half of the same confusion). + +Split it: + +| Concept | Is | Cardinality | +|---|---|---| +| **artifact** | something built and published — an image, a bundle, a package | once per module version | +| **part** | an independently selectable piece of a module's desired state | chosen per node, per assignment | + +A module declares desired state in parts, and produces artifacts. An assignment names the node +and the parts. Delivery builds artifacts once and applies parts per node — and the two words +now carry the two cardinalities that the pipeline already has. + +This keeps what features are genuinely for — per-node opt-in of *some* of a module, which the +work breakdown already calls for — and drops the conflation that makes the current model +confusing. + +## What the mesh is, versus what it hosts + +Tiers 0–3 are the mesh. Tier 4 is everything it carries, and the boundary is stated by +requirement rather than by taste: **a module is part of the mesh if removing it stops the mesh +managing nodes.** A media server does not. An identity provider does — which is why identity +sits in the substrate and not the catalogue, despite being, in every other respect, an +application like any other. + +That test also settles the IT-company goal without a special category. Development, design and +deployment tooling are **workloads** — tier 4, hosted, provisioned, delivered like anything +else. The mesh does not grow a "company" feature; it hosts the tools a company runs on, and +the fact that it runs its own development on them is dogfooding, not architecture. + +## What agents are, structurally + +Self-improvement and self-healing are not a tier. Agents are participants +([ADR 0012](../../02-DECISIONS/0012-agents-are-persistent-employees.md)) that hold identity in +tier 2, act through tier 3 like any other caller, and run as workloads in tier 4. + +This matters for one reason: **an agent must not have a privileged path**. Anything an agent +can do to the mesh, a person can do through the same surface, and anything it cannot express +through the tool surface is a gap in the surface rather than a reason for a back door. Self- +healing built on a private channel is unreviewable, and would be the one part of the mesh with +no human checkpoint. + +## How this is tested + +The lab ([ADR 0016](../../02-DECISIONS/0016-a-lab-node-is-a-virtual-machine.md)) raises the +tree above on one machine: virtual machines as nodes, a real overlay between them, the real +substrate bundle, the real control plane, the real delivery path. + +The tier rule is what makes that affordable. A scenario needing only tiers 0 and 1 is one +virtual machine and a pinned bundle — which is also, exactly, the bootstrap path. **The +hardest thing to test becomes the cheapest scenario to run**, and the first-node path stops +being the one thing nobody exercises until it breaks. + +## What this skeleton does not answer + +- Where the record lives. It is infrastructure by shape and domain by content, and putting it + in the substrate risks recreating a circularity in the one place the design just removed one. +- Whether tier 2's contexts are one repository or several. Open from ADR 0015 already. +- Whether an `edge` node is in the inventory or merely present — which decides whether "node" + is one concept or two. +- The migration. Nothing here says how today's mesh becomes this, and the skeleton is worth + little until that is costed.