diff --git a/02-DECISIONS/0035-one-implementation-several-surfaces.md b/02-DECISIONS/0035-one-implementation-several-surfaces.md new file mode 100644 index 0000000..722c076 --- /dev/null +++ b/02-DECISIONS/0035-one-implementation-several-surfaces.md @@ -0,0 +1,115 @@ +--- +topic: what runs on it +status: accepted +date: 2026-08-31 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0034-the-local-account-owns-the-mesh.md +--- + +# 35. One implementation, several surfaces, and what that costs + +## Context + +The mesh is operated from a command line today. It needs to be operable from a browser and from a +model's tools as well, and the three must not be three different systems. + +**The pattern is already in the code and unnamed.** `board` serves HTTP by calling the same +functions the CLI calls; it holds nothing and decides nothing. What follows makes that the rule +rather than a property of one command. + +**The board is a presentation layer over the control plane.** Not an application beside it holding +a database credential — the thing that shows what the control plane knows, and asks it to do what +a person asked for. + +## Decision + +**The logic lives once, in the context that owns it. A surface is an adapter with no decisions in +it.** + +| surface | for | +|---|---| +| **command line** | a person on a machine, and the recovery path below | +| **HTTP** | the board, and anything else that speaks to the mesh over a network | +| **model tools** | an agent asking the mesh to do something | + +**Every surface refuses identically, because the refusal is not in the surface.** An assignment +that cannot be satisfied is refused by the same resolution whichever way it arrived. The moment a +surface can accept something another would reject, the mesh has two answers to one question and +people learn which to trust. + +**Reading and doing are both exposed.** The HTTP surface is not read-only: managing the mesh from +a browser is the point. This takes the decision +[ADR 0034](0034-the-local-account-owns-the-mesh.md) said had to be taken deliberately — +**a browser login now carries authority over the mesh** — and takes it knowingly rather than +letting it arrive with a feature. + +**The networked surfaces authenticate through an OAuth2 identity provider.** Named by protocol +rather than by product, like every other dependency the mesh takes — AMQP for the bus, S3 for an +object store, OCI for the registry +([ADR 0006](0006-the-substrate-and-the-control-plane.md)). What fills the role today is a module +running Keycloak; what the control plane knows is that it validates a token against a provider +speaking OAuth2, and replacing that provider is a migration rather than a redesign. + +**The command line does not authenticate at all**: it is already behind the machine's own login, +which is what owns the mesh (ADR 0034). + +## What this is not: a kernel every module imports + +**The shared library is the failure this project was started over**, and the difference has to be +stated or it will be rebuilt. The old one is 155 files and 34,636 lines *containing code from +every context* — work-domain logic sitting in the kernel every module imports, each piece landing +there to avoid a cycle between two modules that both needed it. + +**Shared surfaces are not a shared library.** What is shared here is that three adapters call the +same functions. Those functions stay in the context that owns them — provisioning's logic in +provisioning, identity's in identity — and no module imports another's. A surface may call many +contexts; a context still may not reach into another's store +([ADR 0008](0008-a-context-owns-its-store.md)). + +The test, when something is about to be put "somewhere shared": *does this belong to a context, or +does it only belong to the surface?* If it belongs to a context it goes there, even if two +surfaces want it. + +## The loop this creates, and the way out + +**The control plane's networked surfaces will depend on a module the control plane assigns.** +An identity provider is an ordinary module ([ADR 0031](0031-the-control-plane-authenticates-nobody.md)). When +it is down, or being migrated, or misconfigured, the HTTP and tool surfaces cannot authenticate +anybody — including the person trying to fix it. + +**The command line is the way out, and it is why local ownership matters more rather than less.** +It authenticates through nothing, needs no network, and is available on the machine to the account +that owns the mesh. **A mesh must always be operable by somebody standing at it.** + +So the rule: **no capability exists only behind an authenticated surface.** Anything the board can +do, the command line can do. That is not a courtesy to CLI users; it is the recovery path, and a +capability that exists only over HTTP is one that disappears exactly when identity does. + +## Consequences + +**Identity is still not substrate**, and the test still answers no: the control plane runs, applies +declarations and reaches nodes with no identity provider in existence +([ADR 0033](0033-the-substrate-is-a-store-and-a-broker.md)). What is unavailable without it is two +surfaces, not the mesh. + +**Whoever the identity provider admits has authority over the mesh.** That is now a real perimeter +with real consequences, where before it guarded a page that only read. Who may log in, and to +which realm, becomes a decision about the mesh rather than about an application. + +**A surface must not grow an opinion.** The likely erosion is a validation added to the board +because it was quicker there — and then the CLI accepts something the board rejects, or worse the +reverse. Adapters hold no decisions. + +**Three surfaces over one implementation is a cost paid three times if it is not one +implementation.** The reason to write this down now is that the second surface is the cheapest +moment to get it right, and the third is where the drift usually starts. + +## References + +- [ADR 0034](0034-the-local-account-owns-the-mesh.md) — the local account owns the mesh, and the + line this record deliberately crosses +- [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) — the shared library this must not + become +- [ADR 0008](0008-a-context-owns-its-store.md) — a context owns its store, which a surface does + not change diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index c1e7d5d..ad98579 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -105,6 +105,7 @@ python3 00-META/checks/index.py fail if stale - **0024** — [Model access is a provision, and a licence is a thing with a name](0024-model-access-is-a-provision.md) - **0026** — [The mesh has a session of its own, and it is the node session's mechanism](0026-the-mesh-has-a-session-of-its-own.md) - **0027** — [A provision names what the consumer is coupled to, not the role it plays](0027-a-provision-names-what-the-consumer-is-coupled-to.md) +- **0035** — [One implementation, several surfaces, and what that costs](0035-one-implementation-several-surfaces.md) ### How it is built