From 187389b7d1b8d5e3f7b8966fa12f5c4cb3f654b4 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 20 Sep 2026 23:08:28 +0200 Subject: [PATCH 1/7] Hand off the secrets vault to mesh-catalog; open issues 069 and 070 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design 24 flips to in-progress with mesh-catalog as its owner (playbook 04). Starting the build surfaced two gaps the decision did not settle: a module requiring `secret` receives exactly one value (069), and no command can accept an operator's value into a consumer↔vault pair (070). Both opened as issues rather than improvised around. Also fills fixed-by on 067 and 068, which the cycle check refused as resolved with no reference. --- 03-DESIGN/01-to-be/24-the-secrets-vault.md | 4 +- .../00-report.md | 2 +- .../00-report.md | 2 +- .../00-report.md | 57 +++++++++++++++++++ .../00-report.md | 54 ++++++++++++++++++ 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md create mode 100644 04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md diff --git a/03-DESIGN/01-to-be/24-the-secrets-vault.md b/03-DESIGN/01-to-be/24-the-secrets-vault.md index 020ac8c..082fed3 100644 --- a/03-DESIGN/01-to-be/24-the-secrets-vault.md +++ b/03-DESIGN/01-to-be/24-the-secrets-vault.md @@ -1,7 +1,7 @@ --- layer: to-be -status: designed -code: [] +status: in-progress +code: [mesh-catalog] updated: 2026-09-20 decisions: - 02-DECISIONS/0085-a-secret-is-a-provision.md diff --git a/04-ISSUES/067-a-provision-cannot-name-which-provider-serves-it/00-report.md b/04-ISSUES/067-a-provision-cannot-name-which-provider-serves-it/00-report.md index 411feae..749c294 100644 --- a/04-ISSUES/067-a-provision-cannot-name-which-provider-serves-it/00-report.md +++ b/04-ISSUES/067-a-provision-cannot-name-which-provider-serves-it/00-report.md @@ -2,7 +2,7 @@ status: resolved opened: 2026-09-20 located-in: [] -fixed-by: +fixed-by: graduated — hq fc4ab37 (ADR 0084, to-be design 23) amended-design: 03-DESIGN/01-to-be/23-choosing-a-provider.md --- diff --git a/04-ISSUES/068-secrets-have-no-owning-module/00-report.md b/04-ISSUES/068-secrets-have-no-owning-module/00-report.md index 6c63b96..2e2ecbc 100644 --- a/04-ISSUES/068-secrets-have-no-owning-module/00-report.md +++ b/04-ISSUES/068-secrets-have-no-owning-module/00-report.md @@ -2,7 +2,7 @@ status: resolved opened: 2026-09-20 located-in: [] -fixed-by: +fixed-by: graduated — hq fc4ab37 (ADR 0085, to-be design 24) amended-design: 03-DESIGN/01-to-be/24-the-secrets-vault.md --- diff --git a/04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md b/04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md new file mode 100644 index 0000000..047d445 --- /dev/null +++ b/04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md @@ -0,0 +1,57 @@ +--- +status: open +opened: 2026-09-20 +located-in: [] +fixed-by: +amended-design: +--- + +# One `secret` provision yields one value, and a module may need several + +## Symptom, as observed + +[ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md) makes a module's own secret a +provision: the module requires `secret` from a vault and reads the pair credential the mesh +minted for that consumer↔vault pair. A pair has **exactly one** credential — that is the property +[13](../../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md) is built on, and the reason +rotation touches one holder and nothing else. + +A module may only require a provision name once, so a module that requires `secret` receives +**one value**. Counted across the catalogue at the time of writing, a module's own secrets other +than its broker account number as follows: + +| own secrets | modules | +|---|---| +| one | 22 | +| two | 6 (an internal token and an admin password, a user and a password for an outbound mail relay, …) | +| three or more | 4 (a source, an admin and a relay password; a root certificate, its key and that key's password; …) | + +Ten modules cannot be migrated onto the vault as designed, because the design gives them one +value where they hold two or more, and nothing in the record says what they should do instead. + +## Why it matters beyond this instance + +- **The migration in ADR 0085 is "module by module, not a flag day"** — but for a third of the + modules with own secrets there is no target to migrate to, and the gap is silent: such a + module keeps `own-secrets` for the rest and looks migrated. +- **Two candidate answers pull in different directions and neither is recorded.** A module could + derive several keys from its one value (a key-derivation step inside the module, which puts + cryptography into every module that needs two secrets), or the mesh could let a consumer + require several *named* secrets from one vault (which is a pair with several credentials, the + thing 13 deliberately does not have, or several pairs between the same two modules, which the + identity derivation in [ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md) + cannot express). +- **A secret with structure is not one value either.** A certificate authority's root + certificate, key and key password are three things with one lifecycle; a value the controller + mints at random is none of them. The vault's third species — an operator-delivered secret — is + the nearer fit, and it has its own gap ([070](../070-an-operator-cannot-deliver-a-pair-credential/00-report.md)). + +## Open questions + +- Is "one value per module" a rule to keep, with derivation the module's business, or does a + consumer name several secrets and the vault serve each as its own pair? +- If the latter: what is the login of the second pair between the same consumer and the same + vault, given that a login is derived from the (node, module) pair and is what the secret is + keyed on? +- Which modules genuinely need several independent secrets, and which hold two names for one + thing (an admin password and a token that is only ever set from it)? diff --git a/04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md b/04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md new file mode 100644 index 0000000..5a2b7a2 --- /dev/null +++ b/04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md @@ -0,0 +1,54 @@ +--- +status: open +opened: 2026-09-20 +located-in: [mesh-controller] +fixed-by: +amended-design: +--- + +# An operator cannot deliver a pair credential, so the vault's third species has no entry + +## Symptom, as observed + +[ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md) names three species of secret and +gives the vault two: a module's own secret, which the mesh mints, and an **operator-delivered** +secret — a credential for something outside the mesh, which only a person can supply. The design +([24](../../03-DESIGN/01-to-be/24-the-secrets-vault.md)) says the vault *"takes custody of one an +operator delivered"*. + +Under 0085 a secret from the vault is a pair credential between the consumer and the vault. The +controller has one command that takes a value from a person — `secret accept` — and it writes +**only a module's own secret**: one node, one module, one name, sealed to that node's key alone. +There is no command that accepts a value *into a pair*: sealed to the consumer's node **and** to +the vault's node, recorded as `accepted` so a later push does not replace it with a minted one. + +The primitive exists. The sealing package's `Accept` takes a value and two keys, and the licence +adapters already use it to carry an operator's token to both ends of a licence. Nothing exposes it +for an ordinary provision. + +So today an operator-delivered secret can be held in only one of two wrong ways: as an own secret +(un-audited, un-rotatable — the gap 0085 opened to close), or not at all. + +## Why it matters beyond this instance + +- **The vault is one third short of its decision** until this exists, and the third that is + missing is the one with a real leak history — external keys are the secrets that end up in + transcripts and env files. +- **The `made`/`accepted` distinction is only recorded for own secrets.** A pair credential has no + origin column, so even once a value can be accepted into a pair, `rotate` would mint over it — + generating "32 random bytes where a working credential was", the exact fault `secret accept` + was written to prevent for own secrets. Rotating an accepted pair credential must refuse, or + must ask for the replacement value, and neither is designed. +- **The consumer's login is derived, and an external service did not derive it.** An accepted + external key has no `as` the far side knows; a pair credential assumes both ends agree on one. + For the vault this is harmless (the vault creates nothing under that login), but the model + leaks through in what the consumer is told. + +## Open questions + +- Is this `secret accept secret --from …` growing a provider end, or a + new verb on the pair? +- Does an accepted pair credential refuse `rotate`, or does `rotate` become "accept a new value + and deliver it" for that pair? +- Should the origin (`made` / `accepted`) become a fact of every pair credential, so the vault's + ledger can show which of its secrets a person supplied? From baa33515521b033dd84b1a8fff458c637ffcfca9 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 20 Sep 2026 23:55:01 +0200 Subject: [PATCH 2/7] Amend ADR 0085: the vault is a foundation module and holds the root secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recorded on the record, dated, before anything shipped against the sentences that change. The vault is installed at genesis like the store and broker, one per mesh, and holds every secret a module has for itself sealed a second time to an operator key whose private half never enters the mesh — the break-glass path the first version left open, without a key one place holds. Design 24 says how; 07 and 21 say what genesis does not yet do; issue 071 names the fixed credentials the foundation is raised with today. --- 02-DECISIONS/0085-a-secret-is-a-provision.md | 46 +++++++++++ 03-DESIGN/01-to-be/07-the-foundation.md | 6 ++ .../01-to-be/21-the-installation-in-full.md | 7 +- 03-DESIGN/01-to-be/24-the-secrets-vault.md | 78 ++++++++++++++----- .../00-report.md | 39 ++++++++++ 5 files changed, 154 insertions(+), 22 deletions(-) create mode 100644 04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md diff --git a/02-DECISIONS/0085-a-secret-is-a-provision.md b/02-DECISIONS/0085-a-secret-is-a-provision.md index ca0c54e..f322231 100644 --- a/02-DECISIONS/0085-a-secret-is-a-provision.md +++ b/02-DECISIONS/0085-a-secret-is-a-provision.md @@ -2,6 +2,7 @@ topic: what runs on it status: accepted date: 2026-09-20 +amended: 2026-09-20 deciders: jochen reconstructed: false extends: 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md @@ -93,6 +94,51 @@ the vault offers recovery without it is left to the design as an open question. today bakes a password into its own composition must instead require it from the vault — a migration taken module by module, not a flag day. +## Amendment — 2026-09-20, before anything shipped + +Recorded on the record itself rather than as a supersession, by its decider, on the day it was +accepted and before any code was merged against the sentences that change. The original text above +is left as written; this section says what it got wrong and what stands instead. + +**What it got wrong.** The decision treated the vault as a provider like the store — optional, and +one per node — and left the mesh's own root secrets outside it: the store's superuser, the broker's +administrator, the controller's contexts, sealed to a node key and nothing else. Those are the +secrets with no rotation and no recovery, and they are the ones a vault exists for. At genesis they +are not even secret: the foundation raises its store and broker with fixed, well-known credentials +and carries those into the mesh. Leaving that floor in place gave module secrets an owner and the +root secrets none. + +**What stands instead.** + +- **The vault is a foundation module.** It is installed at genesis as part of the foundation + ([ADR 0078](0078-the-store-and-broker-are-modules.md) is the precedent: a foundation piece is + still an ordinary module), not assigned later by a mesh that happens to want one. *"A mesh that + wants no vault runs none"* is withdrawn. A mesh has root secrets, so a mesh has a vault. +- **One per mesh, on the control-node.** *"The vault is node-scoped like every provider"* is + withdrawn. Node scoping ([ADR 0084](0084-which-provider-serves-a-consumer.md)) exists because a + store holds data a consumer is coupled to; the vault holds nothing a consumer is coupled to, and a + second one would be a second place to lose. Consumers on other nodes reach it as they reach + identity. +- **The vault holds the mesh's root secrets under an operator-held key.** The controller mints and + delivers exactly as before; in addition, every secret a module holds for itself is sealed a second + time, to an **operator sealing key** whose private half never enters the mesh. The vault keeps + those operator-sealed copies on its own disk, outside the store, and can hand them out — they are + ciphertext to everything but the operator. This is the break-glass path the original text left + open, and it does **not** reintroduce a key one place holds: the mesh holds blobs it cannot open, + and the operator holds a key with nothing to open until given a blob. Recovery needs both. +- **Genesis mints real root secrets and seals them to the operator key first**, so the fixed + credentials the foundation is raised with are replaced before the mesh is handed over. + +**Unchanged.** A module's own secret is a `secret` provision the controller mints and the vault +records; the provisioned-pair path of [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) +is untouched; the vault stores no plaintext, ever. + +**What it costs.** An operator key is a thing a person must keep, and a mesh whose operator key is +lost has root secrets that can be rotated but not recovered — the same standing as today, stated. +Sealing every own secret twice is a column and a call. Genesis grows a step. A module's +vault-provided secret (a pair credential) is not yet sealed to the operator key; that is the next +increment, not this one. + ## References - [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — identity is a module; this is the diff --git a/03-DESIGN/01-to-be/07-the-foundation.md b/03-DESIGN/01-to-be/07-the-foundation.md index 87d7751..7b6587c 100644 --- a/03-DESIGN/01-to-be/07-the-foundation.md +++ b/03-DESIGN/01-to-be/07-the-foundation.md @@ -246,6 +246,12 @@ host's vocabulary grows by one shape rather than by one resource type per founda the module that provides one. - **Whether one host can raise all three.** The claim under stage 2 of [the node host](05-the-node-host.md), never proved. If it is false, the tier boundary moves. +- **The vault as the fourth piece.** [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), + amended, makes the vault a foundation module: genesis makes the operator key before anything + is minted, replaces the fixed credentials the store and broker are raised with, and installs + `mesh-vault` beside the adopted store and broker ([24](24-the-secrets-vault.md)). The controller's + half exists; the installer's does not yet, and the bundle still raises the foundation with + well-known credentials ([issue 071](../../04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md)). - **How the foundation is updated once a mesh exists.** Pinned by hand at bootstrap; afterwards the controller could deliver it like anything else, and nothing says whether it does. diff --git a/03-DESIGN/01-to-be/21-the-installation-in-full.md b/03-DESIGN/01-to-be/21-the-installation-in-full.md index 91c5dae..80a6406 100644 --- a/03-DESIGN/01-to-be/21-the-installation-in-full.md +++ b/03-DESIGN/01-to-be/21-the-installation-in-full.md @@ -5,7 +5,7 @@ code: - mesh-host internal/bootstrap - mesh-host cmd/mesh-bootstrap - mesh-lab test/integration/one-node-mesh.test.ts -updated: 2026-09-15 +updated: 2026-09-20 decisions: - 02-DECISIONS/0067-genesis-is-a-pivot.md - 02-DECISIONS/0073-the-installer-carries-a-builder.md @@ -118,6 +118,11 @@ told anything. this is the lab being honest rather than the mesh being broken. What is missing is the step that puts the shipped unit on the machine. +**The foundation is raised with fixed credentials, and they stay.** The store's superuser and the +broker's administrator are constants in the bundle, carried into the mesh by `secret accept`. No +operator key is made at genesis, so nothing minted during installation is sealed to one +([24](24-the-secrets-vault.md), [issue 071](../../04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md)). + **Nothing asserts a mesh was installed this way.** A claim that a machine was brought up by this procedure cannot be contradicted by anything afterwards. diff --git a/03-DESIGN/01-to-be/24-the-secrets-vault.md b/03-DESIGN/01-to-be/24-the-secrets-vault.md index 082fed3..9b1070e 100644 --- a/03-DESIGN/01-to-be/24-the-secrets-vault.md +++ b/03-DESIGN/01-to-be/24-the-secrets-vault.md @@ -1,7 +1,7 @@ --- layer: to-be status: in-progress -code: [mesh-catalog] +code: [mesh-catalog, mesh-controller] updated: 2026-09-20 decisions: - 02-DECISIONS/0085-a-secret-is-a-provision.md @@ -57,21 +57,60 @@ This is what replaces the "generated value that nothing owns". A module's own pa a special kind of thing injected by the synchroniser and becomes a provision with a provider, a holder, and a lifecycle — the same shape as everything else the mesh grants. -## The vault is a module, and node-scoped +## The vault is a foundation module, one per mesh -The vault is an ordinary module. A mesh that wants one runs it; a mesh whose modules require no -secret of their own runs none — the same test identity meets -([ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md)). It is provisioned -the ordinary way, and it does **not** mint the mesh's delivery credentials — the controller does -that, because the controller must mint in order to deliver any provision, the vault's own included. -The vault mints secrets *for modules*, downstream of its own existence, never the credential that -delivers it. +The vault is an ordinary module — built, assigned and upgraded like any other — and it is part of +the foundation: installed at genesis, the way the store and broker are raised first and then +adopted as modules ([ADR 0078](../../02-DECISIONS/0078-the-store-and-broker-are-modules.md)). A +mesh has root secrets, so a mesh has a vault; it is not something a mesh opts into +([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), amended). It is provisioned the +ordinary way, and it does **not** mint the mesh's delivery credentials — the controller does that, +because the controller must mint in order to deliver any provision, the vault's own included. -Like every provider it is node-scoped -([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [23](23-choosing-a-provider.md)): -each node may run its own vault, and a module's own secret is held by the vault on the module's -node, selected the same way any provider is. There is no single mesh vault holding everything, for -the same reason there is no single mesh store. +There is **one vault per mesh**, on the control-node, reached from any node the way identity is. +Node scoping ([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), +[23](23-choosing-a-provider.md)) exists because a store holds data a consumer is coupled to; a +vault holds nothing a consumer is coupled to, and a second one would be a second place to lose. + +## The root secrets, and the operator key + +The mesh has secrets of its own that no module requires from anybody: the store's superuser, the +broker's administrator, the controller's credentials to its contexts, the sealing key of every +node. Until now each was sealed to the node that uses it and to nothing else, so a node whose key +was gone took them with it — and at genesis they were not even secret, the foundation being raised +with fixed credentials that were then carried in. + +The vault's second job is to hold these, and it does so without holding a value: + +- **An operator sealing key.** A keypair made once, by the operator, whose private half is written + to a file the operator keeps off the mesh and whose public half the mesh records. It is made + before anything else is minted, so that everything is sealed to it. +- **A second seal.** Every secret a module holds for itself — minted or accepted — is sealed to its + node as before and, in addition, to the operator key. What the mesh stores is one more blob it + cannot open. A secret made before the key existed has no such copy and cannot get one, the + plaintext being gone; the mesh says which those are rather than letting the export pass for + complete. +- **The export.** All operator-sealed copies, the key's public half and its fingerprint, and the + list of what is not recoverable, as one document. The vault declares that it *keeps* this, and + the mesh writes it onto the vault's own disk as an ordinary declared file — ciphertext to the + machine that holds it and to the bus it crossed. The same document can be written out by the + operator to keep beside the key. +- **Recovery.** The operator, holding the private key, opens one secret from the export or from the + store and gets it as a file, never on a terminal unless asked for. It needs the key and a blob; + the mesh has the blobs and no key, the operator the key and no blob until given one. Nothing in + this reintroduces a place that can open everything, which is the property + [ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) was + built to keep — so the break-glass question the first version of this design left open is + answered by it. + +**Genesis** makes the operator key first and mints real root secrets in place of the fixed ones the +foundation is raised with, so the mesh is handed over with nothing well-known in it. That step is +the installer's and is not yet built; until it is, the fixed credentials are the as-is and are +said so in [21](21-the-installation-in-full.md). + +What is not yet sealed to the operator: a module's vault-provided secret — the pair credential of +[13](13-credentials-and-their-rotation.md). That is the next increment, the same column and the +same call on the pair table. ## Beyond generate and hold @@ -80,13 +119,10 @@ secrets subsystem whose surface names the operations a vault is responsible for backing it up, verifying it is what it should be, and a break-glass recovery for when the normal path is unavailable. These become the vault module's, specified against it rather than scattered. -One of them is left open on purpose. **Break-glass must not reintroduce a key that one place -holds.** The whole point of sealing a secret to the machine that needs it, asymmetrically, is that -no single place can open everything -([ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md)); a -recovery path that keeps a master key would undo exactly that. How the vault lets an operator -recover a secret without becoming the thing the sealing was designed to prevent is a question this -design opens and does not yet answer. +Break-glass is answered above, and by the property it had to keep: **it does not reintroduce a +key that one place holds.** The vault keeps blobs sealed to the operator; the operator keeps a key +with nothing to open until handed a blob. Locating and verifying are the vault's tools, by +fingerprint; backing up is the export. ## What this changes for a module diff --git a/04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md b/04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md new file mode 100644 index 0000000..433826e --- /dev/null +++ b/04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md @@ -0,0 +1,39 @@ +--- +status: located +opened: 2026-09-20 +located-in: [mesh-host internal/bootstrap, mesh-host examples/foundation-first-node.lock] +fixed-by: +amended-design: 03-DESIGN/01-to-be/24-the-secrets-vault.md +--- + +# The foundation is raised with fixed credentials, and they stay + +## Symptom, as observed + +The foundation bundle raises the store with a superuser password that is the literal word +`bootstrap`, and the broker with its image's default administrator, `guest` / `guest`. The +installer then carries both into the mesh through `secret accept`, sealed to the control-node's +key, marked `accepted` so the mesh will never replace them — which is correct for a credential +that already created the databases, and means the well-known value is now permanent. + +Every module's own secret minted afterwards is random and sealed. The two that everything else +rests on are not random, and there is no operator key at genesis for anything to be sealed to. + +## Why it matters beyond this instance + +- **These are the root secrets.** A mesh whose store superuser is a published constant is a mesh + whose every provisioned credential is one connection away, from any node that can reach 5432. +- **It is invisible.** `secret accept` reports the value as sealed to the machine and unreadable by + the mesh, which is true, and says nothing about where it came from. +- **Rotation cannot fix it later.** An accepted own secret is never remade by the mesh, and there is + no `rotate` for own secrets; the only path is to change it on the server by hand and accept it + again, which is the manual rotation the as-is design records as having taken services down. + +## What closes it + +[ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), amended, and design +[24](../../03-DESIGN/01-to-be/24-the-secrets-vault.md): genesis makes the operator key first, +mints real credentials for the store and broker before the bundle raises them (or changes them +on the running servers before handing over), accepts those, and installs `mesh-vault` so the +operator-sealed export exists from the first push. The controller's half — the key, the second +seal, export and recovery — is built; the installer's half is not. From 050a2d08e833fb7eedef7f4c3fbb57f97f39db65 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 00:27:01 +0200 Subject: [PATCH 3/7] Playbook 07: a feature worktree needs the siblings the lab reads A bed run from .work//mesh-lab derives mesh-tools and mesh-sdk by sibling path and fails at once when the directory holds only the touched repos. Detached worktrees on main, never symlinks. --- 00-META/process/07-feature-branches.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/00-META/process/07-feature-branches.md b/00-META/process/07-feature-branches.md index 8fb9799..3ca0387 100644 --- a/00-META/process/07-feature-branches.md +++ b/00-META/process/07-feature-branches.md @@ -28,6 +28,16 @@ One feature is **one branch name**, **one worktree per repo**, **one MR per repo git worktree add .work// -b feat/ origin/main ``` Parallel features never collide, and no shared checkout is edited. + + **Add the untouched siblings the lab reads.** The lab beds find the other repositories by + sibling path from the lab checkout (`../mesh-tools/module.json`, `../mesh-sdk`, …), the way + the main layout has them. A `.work//` directory holding only the touched repos fails a + bed at once with `no manifest for mesh-tools at …/.work//mesh-tools/module.json`, after + genesis has already passed. Give the directory those repos as **detached worktrees on `main`** + — never symlinks: + ``` + git worktree add --detach .work//mesh-tools main + ``` 3. **Commit as you go — locally.** Increments land on the one branch. Nothing is pushed and no MR is opened mid-feature. 4. **Finish, then publish.** When the whole feature is done — every repo, tests green — push @@ -52,6 +62,8 @@ The end state is visible, and its absence is the smell: - After a feature merges, `git branch -r | grep feat/` and `git worktree list` return nothing for it. A surviving branch or worktree means step 6 was skipped. +- A bed run from `.work//mesh-lab` that fails naming a `.work///…` path it + cannot find is a missing sibling worktree, not a mesh fault. - More than one open MR in a repo that share no feature name, or a stack of MRs none of which is merged, is the failure this playbook exists to prevent — stop and consolidate before opening more. From 8607d2111076b2f49d7326e24d2ad3810117eef8 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 00:36:30 +0200 Subject: [PATCH 4/7] Design 24: pair credentials are sealed to the operator too --- 03-DESIGN/01-to-be/24-the-secrets-vault.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/03-DESIGN/01-to-be/24-the-secrets-vault.md b/03-DESIGN/01-to-be/24-the-secrets-vault.md index 9b1070e..ff05586 100644 --- a/03-DESIGN/01-to-be/24-the-secrets-vault.md +++ b/03-DESIGN/01-to-be/24-the-secrets-vault.md @@ -108,9 +108,11 @@ foundation is raised with, so the mesh is handed over with nothing well-known in the installer's and is not yet built; until it is, the fixed credentials are the as-is and are said so in [21](21-the-installation-in-full.md). -What is not yet sealed to the operator: a module's vault-provided secret — the pair credential of -[13](13-credentials-and-their-rotation.md). That is the next increment, the same column and the -same call on the pair table. +A module's vault-provided secret — the pair credential of +[13](13-credentials-and-their-rotation.md) — is sealed to the operator the same way, as is every +credential a provider grants; the export names each entry by the node and module that hold it and +the name they know it by, and says whether it is a module's own secret or a pair credential, so +recovery addresses both alike. ## Beyond generate and hold From 1bce3f33a86e730a244f698181234c893e9e97a6 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 00:50:56 +0200 Subject: [PATCH 5/7] Designs 07 and 21 and issue 071: genesis now makes the root secrets The installer half of the amended ADR 0085 is built and proven by the genesis bed's root-secrets step; the foundation design closes its open item and the installation design says what the installer does and what it still cannot check. --- 03-DESIGN/01-to-be/07-the-foundation.md | 15 ++++++++------- .../01-to-be/21-the-installation-in-full.md | 16 +++++++++++----- .../00-report.md | 10 ++++++---- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/03-DESIGN/01-to-be/07-the-foundation.md b/03-DESIGN/01-to-be/07-the-foundation.md index 7b6587c..2a1c898 100644 --- a/03-DESIGN/01-to-be/07-the-foundation.md +++ b/03-DESIGN/01-to-be/07-the-foundation.md @@ -8,7 +8,7 @@ code: - mesh-catalog modules/postgres - mesh-catalog modules/lavinmq - mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh) -updated: 2026-09-17 +updated: 2026-09-21 decisions: - 02-DECISIONS/0004-a-node-and-how-it-joins.md - 02-DECISIONS/0078-the-store-and-broker-are-modules.md @@ -246,12 +246,13 @@ host's vocabulary grows by one shape rather than by one resource type per founda the module that provides one. - **Whether one host can raise all three.** The claim under stage 2 of [the node host](05-the-node-host.md), never proved. If it is false, the tier boundary moves. -- **The vault as the fourth piece.** [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), - amended, makes the vault a foundation module: genesis makes the operator key before anything - is minted, replaces the fixed credentials the store and broker are raised with, and installs - `mesh-vault` beside the adopted store and broker ([24](24-the-secrets-vault.md)). The controller's - half exists; the installer's does not yet, and the bundle still raises the foundation with - well-known credentials ([issue 071](../../04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md)). +- ~~**The vault as the fourth piece.**~~ **Closed 2026-09-21** by + [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md) as amended: genesis makes the + operator key before anything is minted, raises the store and broker with credentials it made + rather than the template's, adopts both as modules with those credentials, installs + `mesh-vault` beside them, and writes the operator-sealed export next to the key + ([24](24-the-secrets-vault.md), [issue 071](../../04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md)). + Proven by the one-node genesis bed's root-secrets step. - **How the foundation is updated once a mesh exists.** Pinned by hand at bootstrap; afterwards the controller could deliver it like anything else, and nothing says whether it does. diff --git a/03-DESIGN/01-to-be/21-the-installation-in-full.md b/03-DESIGN/01-to-be/21-the-installation-in-full.md index 80a6406..4183861 100644 --- a/03-DESIGN/01-to-be/21-the-installation-in-full.md +++ b/03-DESIGN/01-to-be/21-the-installation-in-full.md @@ -5,7 +5,7 @@ code: - mesh-host internal/bootstrap - mesh-host cmd/mesh-bootstrap - mesh-lab test/integration/one-node-mesh.test.ts -updated: 2026-09-20 +updated: 2026-09-21 decisions: - 02-DECISIONS/0067-genesis-is-a-pivot.md - 02-DECISIONS/0073-the-installer-carries-a-builder.md @@ -118,10 +118,16 @@ told anything. this is the lab being honest rather than the mesh being broken. What is missing is the step that puts the shipped unit on the machine. -**The foundation is raised with fixed credentials, and they stay.** The store's superuser and the -broker's administrator are constants in the bundle, carried into the mesh by `secret accept`. No -operator key is made at genesis, so nothing minted during installation is sealed to one -([24](24-the-secrets-vault.md), [issue 071](../../04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md)). +**The foundation's credentials are the installer's, not the template's.** The template still +carries a fixed store password and the broker image's default administrator, because it is applied +raw by beds that raise no mesh; the installer replaces both with values it makes once and keeps at +the paths the store and broker modules declare, rewrites the produced bundle to use them, and +writes that bundle at 0600. After enrolment and before the first secret is accepted it makes the +operator key beside the bundle and gives the mesh its public half; the run ends with the +operator-sealed export beside the key ([24](24-the-secrets-vault.md), +[issue 071](../../04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md)). +What is not yet true: nothing rotates those two credentials afterwards, and the operator key is a +file a person must carry off the machine — the installer says so and cannot check it. **Nothing asserts a mesh was installed this way.** A claim that a machine was brought up by this procedure cannot be contradicted by anything afterwards. diff --git a/04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md b/04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md index 433826e..0661961 100644 --- a/04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md +++ b/04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md @@ -1,8 +1,8 @@ --- -status: located +status: resolved opened: 2026-09-20 located-in: [mesh-host internal/bootstrap, mesh-host examples/foundation-first-node.lock] -fixed-by: +fixed-by: mesh-host feat/secrets-vault (ee0c8b8, genesis root credentials + operator key + vault); mesh-controller feat/secrets-vault (e140ed5, 565f144); mesh-catalog feat/secrets-vault; proven by the one-node genesis bed step V5 amended-design: 03-DESIGN/01-to-be/24-the-secrets-vault.md --- @@ -35,5 +35,7 @@ rests on are not random, and there is no operator key at genesis for anything to [24](../../03-DESIGN/01-to-be/24-the-secrets-vault.md): genesis makes the operator key first, mints real credentials for the store and broker before the bundle raises them (or changes them on the running servers before handing over), accepts those, and installs `mesh-vault` so the -operator-sealed export exists from the first push. The controller's half — the key, the second -seal, export and recovery — is built; the installer's half is not. +operator-sealed export exists from the first push. Built on `feat/secrets-vault` across mesh-host, mesh-controller and mesh-catalog, and proven by +the one-node genesis bed: the template's password is refused by the store, the export and the +vault's copy hold no plaintext, and the superuser recovered off the mesh with the operator key +opens the store. From ffd759268711becf5e76ec025c01749e5330f743 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 01:16:54 +0200 Subject: [PATCH 6/7] Design 24: the export says what an earlier key opens --- 03-DESIGN/01-to-be/24-the-secrets-vault.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/03-DESIGN/01-to-be/24-the-secrets-vault.md b/03-DESIGN/01-to-be/24-the-secrets-vault.md index ff05586..badb34a 100644 --- a/03-DESIGN/01-to-be/24-the-secrets-vault.md +++ b/03-DESIGN/01-to-be/24-the-secrets-vault.md @@ -90,8 +90,10 @@ The vault's second job is to hold these, and it does so without holding a value: cannot open. A secret made before the key existed has no such copy and cannot get one, the plaintext being gone; the mesh says which those are rather than letting the export pass for complete. -- **The export.** All operator-sealed copies, the key's public half and its fingerprint, and the - list of what is not recoverable, as one document. The vault declares that it *keeps* this, and +- **The export.** All copies sealed to the mesh's current operator key, the key's public half and + its fingerprint, and two honest lists beside them: what is sealed to an earlier key the mesh has + since replaced, openable with that key alone, and what has no operator copy at all. As one + document. The vault declares that it *keeps* this, and the mesh writes it onto the vault's own disk as an ordinary declared file — ciphertext to the machine that holds it and to the bus it crossed. The same document can be written out by the operator to keep beside the key. From 61f61b5d068b2c275755ec84d6d7c242d5093a7c Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 01:52:53 +0200 Subject: [PATCH 7/7] Reconcile the open issues against main An audit of the six code repositories found eleven open issues fixed on main with commits and beds to show (025, 027, 033, 036, 037, 040, 045, 047, 050, 052, 053), three partly (007, 026, 035), nine not (020, 031, 041, 046, 049, 054, 064, 065, 066) and one whose fix would live outside those repos (006). Resolved ones name their evidence; partly ones say what remains; 041 records that the exposure has widened since it was reported. --- .../checks/__pycache__/cycle.cpython-314.pyc | Bin 0 -> 12092 bytes .../00-report.md | 1 + .../00-report.md | 4 ++-- .../00-report.md | 2 +- .../00-report.md | 4 ++-- .../00-report.md | 4 ++-- .../00-report.md | 2 +- .../00-report.md | 4 ++-- .../00-report.md | 4 ++-- .../00-report.md | 4 ++-- .../00-report.md | 9 +++++++++ .../00-report.md | 4 ++-- .../00-report.md | 4 ++-- .../00-report.md | 4 ++-- .../00-report.md | 4 ++-- .../00-report.md | 4 ++-- 16 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 00-META/checks/__pycache__/cycle.cpython-314.pyc diff --git a/00-META/checks/__pycache__/cycle.cpython-314.pyc b/00-META/checks/__pycache__/cycle.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d78646152b63d0f664fbe0b1e48af959d5b4d9ea GIT binary patch literal 12092 zcmcgSTTmNUmbdjlPmp9D;uX_?!3Yp<$2NAH@DQ<$4WtE)9b-U94Ja0p(k+ao*_mbM zXTZr0CX+4ju4fH3t})q~t?|sHDpc)mm8UjS$;W;`Bm?eHmD%0-+1i>aJ0v^1`Pn`9 zwpx;jY|m6}(k1od_POVt*FER-J+~$+Q;*;%K6CGlmr4=(cl;n9n)uD$F$$qs#Gnuw zMhta^3Q^>$2r0-_8B&s~Dx|us=p*=K6wkr>Kwtt~$6H;i`vgCZn{XP!^+tvx!l|+010X*>d>+qd9f~Ax|Ml2whh7 zRZ19B&!y)~9q{S?h}0_Wo0lW?#1aBi>Q4ft_;C}1DMWC-2~^6U!6t}gOG3n5nsNSFk= zy&*s%vqo37z3S-ccM^i_b~-c(@8Ac3b+iMx2}6s2f}Z3sTF&JW=z!lB){pZ6e`v}T z3UPcbJ;u3R(=aq1WLltoUc#-PP5~lpfcyv^&L?mn)4@p>A%*jg2Y5H9Gz=tK- zxd5qrU=8}|QHjph(K@<~?(em?I|z2aYYK1zNj}acgn&(~i@0{GM&(!OihDn-i>fpqJLRh?#4PvR+0bXJ}0u2Q2lf;QQg}#6r>;#Cx)PYgK zZ7qmdYPEa)AZtlXA(#(98ONVUAy?v3>P9dCrvcwp2?=SokA(q6oG2iVG-A%HKrmoI z?!dGkc(&V9u$K7)#0G1@I03>Hbd7m^-cXq4u6YF@whY^=%MU+chU0CR&%_K%!>EfM zo#0)b={TS937bL_P&0}xdI|(BnGmtX6pBJFeu4wjjVnwdM8E^fdKcpM>vFrfAkYXD zxdXfhQ_48nI!?E>pL5{pF(vVYy-h3a_F~AfFtHz^LNNNnLdw7?6ACCxvMIYAOs=G4 zcoz)tAq*9FQ`p9*v8~l&)wr-wIX(rqMB!ne{91_fdxUynI>^C%1KS2?;qvs-~-o*?U{50{CI}UOnSi^N?s=} zDS@5{V1MG{Xs@3RasikqK-(ntK;Sw$IiGio<3WeMFx}JJPbYi`J(@5&NfH8hhcKSg zS78PwEqRpYz^8`kDp$h0;t8Vg#YItQR;p&xHj0RoApHR6qG9j+rEe?qg6?!Z^GR(AHx7RyfU~T|?%= z(fOkbFVDZc+;S%;YN*^$Rqnwn^n@fZ7a$4M-VmHY5(z1>5b+ts(Kv5?SX%oIIZp^u zLs*{V=u1dNZb7INF-kTSMw@&m-^uw+P{Aq=#~UYL%woi-$`PyRGsuw1>o6r}DD?ov z_Xm}%GF^7YsAV`aa!_532n92qOK}F9A?r1$VKp*M9_U?+SassrhMxx=wUAZGM_-z} zkJUVz1DV1S)a^v*xzuS=sME4qnHouA-64cn?X$;w%6o-OM_-zZRMt2DSFEltTjn-- zWwdf{nI6wLk(RD6Hwh)*F^T*(0o^M>4^xbhLTNRrmq#$80sXY14oxe%su6ng@18}k zj6W_N&=jkG7JQGQ8|IM?9R%z_{h*@~NOn-&CD2dD7)TBc7F>+#K=1K)c7z7sW!3Q~ z5ESmsg5^#tN(dG&vSmho-wz@AV%zz1^cc9-E8r_bGaTn9QElAILmCyD34n{YLsIC+ zsT;Vu?_A-+tu*hN0WaX2=Ila1@O(7Q(Nh4= z*9x9KFg-B|k%W(r!ucR61wV_^2b`}90PgbOXyt?aKu-ptt0elMlh=<)vEgv@1_Uqg z*bu=25X>R(4RKS#2^u1T+vRidIQ~Ee508YxI{jDb>Cm|S*n5-iE5JGij-4SPw;{1pr{stkN}Apt{?z<;1TEx zih3ZI4+%IA#PoT{oy|Zzw5SR}SQn`&QB86PQ3(M~RDvc%6&8o6!n})$D_2D|5tv9_ z6|jS zvLkZ?I|lQ%p=i@kwA`_6pg%FtD+70jSBLL)Y*!rnq~h34O~XCQy{5Gj+chUQYfi2^ zV>NA2Ywu=F+luR>!Cw!4bn%vMnYyjHsrg$SG;UgZ=Q`d#y_0$1#@O4JcB}>X>9^M6 z8_Ks&KYjsO${rm=M$_!s|M}d2^7|;k2r~RlzO~Di@tc+$IJ;A0HjYF%R_(39X%?}l zFA>q8A%3ul$bsX{IEgcLs2t%KC>^XK>4W7sAo~#yfoBx}uSvqoo~aMV;eChfITILw zN{AwgI;tGJNIBy1xHh$@ghW}?!zZpEtP?(hu(=6fd0?{qK@6hg%i+RT;1l>gB;byf zm2qoRW9TpsAqt(xFCB22MLXKe1>L;vM)_M=vu!&X?f1hwCd-^|*Ic;Vc+0bGF8{<_ zKG(RRI{kk3z1Mz~_d)S5ia$8`%Y$o;Yvs^*pMevP*FoIyPaN3V@z{HMGeR!&EU*zU zv*u=sL006`1`$(7f-MBI8Bh;@AemSUCDfHJ*(!Zb@JlD&Axs`Q7zIl)$`)({aL6i= zuY@8R&=|T^yl(8@>5(0qxV$=;v7)pfuBur4ns>Z=iLCKK*uu4cl;#@<3 z?}R|2N*92b40#Mzacl;bYD!ZuXr9-nRbhlBAy&~!8BvoeIo?zHCBkn|&8mkEND@Qn zI7}B0gt`RfdR9#^8&WiwfR^d~+L#K4!cvNd+t{XMbJKh>&Uc&b%Bsp&jf8h@0g6VOl9w3=za8uFB54L+geY+2f}b|xUH zOlUdhX_^6@H>T)(exJ^B_hXIg{KpBM=RFN`_s_poJzGJ~`OnbvXWyuv^Zz70C!n99 z=Yo_OJ-SaTg|b$H@Zprucj~Mzk|AJLXP8kJd~*ji&+)@WG7rh?b9$r+zRBpoYGfK@ zE@evNHgqmwL&dBnz2E2_mTagSd=d5@^(d(Qmi)#G%t84xS*BmUf;`VY-I^}8oSB{TmeCOSCh3q9p`h-Y%>rQNeJu<4Mn_CL z*K1|A;rfk^o;IR=8z2E2l>wa zBVjRy6l*No4~2_lsiQp`HWiXplIYb=qSqlAZ~EdGQ^6XBk}y>=bn+_C&!CYt$}`Rb z*%CxOV;}zh+jk_Fu+CFVWiSS7QlDf@>AX`=#WVpn4`9no zpTV-3a{pk~5dP*N2b(q2B;U)Dk~I;}B0;ij*1onEq3!=mj#)BY@)aaC9+%XjEUCUD zGL^}zZ0o=`SUCP}3M$Cg(-Fvts#2ma~w^($CJCp2&>A3h_|_+bPPCt@(+?7)%3RDBe2< zr<2qyaw^g-$1tf2FNv>q{7uPFEEWH@OpL?8!i!BAQP6Zm(MtE&6g*BZNcjWSN8s}w ziOQMDu&5n(d41Eke2f<=Dl2EDB{}67EEHO%aEb0DE^zsTQ+3cZ2rvT9g8v(@n3xw# z*G+faYlnX7{blw>{*f*1i)0PtD>d165YZ({?GaTq>;{PFpitR)-hpqkpr)0w>?f*8 z-94fs7rc-f$vFWlu(%8+*WPifm;q}(upuC#k*;*agqv~Q3s(vQWC34RZ>kAOGKY6Bt}Y~96tF5GfRq$5v^SOj+8$MEVoPL=Xg;yVrXczGY+St zjp;ZY$$D~IiKt6duf+_hOdTmtG!X&Bi`NMekV=s4p%)7#^W=q@$bk6$MCpoy32B8uv2DEx^!Sd^dU1=#Ds8^OqUoICUQ6Q9q4 zQ>3Vhbp1}tckP#8F?`52bh)Cn#6BplIuuEl%)Q4SAKTQ_Qll9kDT>nmN28hcAmu*2cu#JXP!V_#YR7aY~NUd~* zCs8f05^VGsEX7|T)rQm!7ZNNFtA!|1`}K~AEjICe3%G<11VgdHrm!g@S{Rp1%KX-+f35FoDpY# zTmOL5;UsIcViuV&iAK>Pn{l!!L#ltn5-=Cy@nQ^b#rWVc$YXECL+D35E>YsW1TQLG9uHrKU*MX9s3zqaQR#zXi8=^1q>#8+*>0tQ!N4ihsqBt|E1(Xy9{Z{zA^K|Ki{<$FAuDA zZ(AEau{J!+u`a7N4l=PE=T3IP#tSF5vQK>I__$)@wZZMq3$e}%1mF>(O4_KMjx%2> zsgkqQ9}tz>O?^>{a#`xJ63gR@YGkp#8(Ip#6TWlcu65OVw|KSqXP$fA_p|T&qoo5; zE4yVLgldqvXt^^Abs;rWY_7lb&}h1$+Q_Sn84u$kk0o!ZV6kBNSk!W8E@Q`FxpfdK zQ2?{3`qlV9j(<4(;8g!c-ayRA!n=)JTg+g4zhYgz{%6}w=RRpV7e{t{z3V|)bLr9$S&+0`*04dh*ZTZ{9e4AzFGdYJGjn zeCbc6KUPp3GeA`<&Opq;J~%bFk#`|xya>eIxU^-UcXJDukE~Qhb8F_hKFcnM8^Oyl z^DCbg(l;`8tp!Uni!)2#Tm0UgvAYwi6L)>9zV})17AFNCj$ZymU8 zy=lE&e6#qDXRUiZdwo1w;E0;Kb}hL}1@9ExYPx;&=F!_HZ=U>_YHe!WxzWx$hYLw;+ z)ax>y1 zU<6C{lJ*_#MgdeV-*5k*^A}*a$LHEUD=oXxxO8;!=yKz`t!oOH;Q&|kP7%zV_5LkO z`&`?O(Xw!6{>-~$xH7t(SGk#2x$@@Snbn!KH-8d|<~3~@oA;FHKnL~M1eEO^kjR6X z@#>Gyua!P1u8o@Q+vesCbMw6ebLvmCUz+Q{HC5fBZn^wNS#xc>rh?_tTh47$Y0Ol5 z*R|@3zr9s<`|!=f+lAGeh1F|~(L(!{xo%H`a!z42GFZEB0>RP+A1 z5VpeCw+tmKom+>Fkf{MP-TFH9Ux>=QL_xRG1}F;ajBj@?oVgxnXvw$eyVJ5(bkDQi z70v628lfEQ*wtsg?OE_%_uf+AD(Y?HP2-*ZwG->g_2Fp#xoFn;sJ;i(tLvqHmlpD+ z_419Ed!zY%(X7{^dIprN1G~yVhOD`Xw`xI?I_o1PQXBtZL|XH%HghAZ{X^%+`TuTpHlX+LA+t43dKl(u59#eHZG#E*~ENVwau!kQRIYkn4 zt(18{0M){dC%k9DBkEjZu;~+WTo2y?F@T3f1w^2wc!vWu--%CMa1!ksf#cxx8ljbCUW6~_?} z(>w0