Merge remote-tracking branch 'origin/main' into design/335-a-busy-container
# Conflicts: # 03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md
This commit is contained in:
+3
-1
@@ -257,7 +257,9 @@ and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 02
|
||||
only by a person (ADR 0230).
|
||||
- **share** — a folder one node offers to the mesh's other nodes over the private network: the provision
|
||||
`nfs-share`, provided by the holder of the node seat `node-nfs-server`, one share per folder
|
||||
(ADR 0263).
|
||||
(ADR 0263). A share may also be granted to the machine's local network (ADR 0341). An **SMB share** is
|
||||
a folder the holder of the node seat `node-smb-server` serves to devices on the local network, and is
|
||||
not a provision (ADR 0341).
|
||||
- **mount** — a share, or an occasional source, made visible at a path on one node by the holder of the
|
||||
node seat `node-mounts`, as a mount unit and an automount unit, mounted on access (ADR 0263).
|
||||
- **occasional source** — a share on a device that is not a node and comes and goes, such as a USB disk;
|
||||
|
||||
@@ -22,8 +22,9 @@ code:
|
||||
- mesh-host internal/apply (the service that reflects a rule set; a whole file handed to its new owner)
|
||||
- mesh-controller cmd/mesh-controller/nodes.go (a token issued for a machine's tunnel key, ADR 0169)
|
||||
- mesh-host cmd/mesh-host/join_tunnel.go (a machine joins through the tunnel, ADR 0169)
|
||||
updated: 2026-10-08
|
||||
updated: 2026-10-13
|
||||
decisions:
|
||||
- "ADR 0340"
|
||||
- "ADR 0247"
|
||||
- "ADR 0226"
|
||||
- "ADR 0223"
|
||||
@@ -1070,6 +1071,19 @@ One value, three readers:
|
||||
| `internal` | the machine port, to the private network | the internal name | the mesh's own authority |
|
||||
| `public` | the machine port, to anywhere | the public name | the public authority |
|
||||
| `both` | the machine port, to anywhere | both names | each name's own authority |
|
||||
| `local-network` | the machine port, to the private network and the machine's local networks | the internal name | the mesh's own authority |
|
||||
|
||||
*2026-10-13.* **A port may be reached from the machine's own local network, and only its private
|
||||
part** (ADR 0340). A home server serves devices on its own network that will never be nodes, and until
|
||||
now the only value that reached them was `anywhere`, which says the internet. A new source,
|
||||
`local-network`, and the reach of the same name mean the networks the machine is attached to through
|
||||
its uplink, as the node-engine reports them among its facts. Only networks wholly in a private range
|
||||
count (RFC 1918, and unique local addresses for IPv6); a public subnet the machine sits on is never
|
||||
counted, so the source cannot open a port to the internet. The filter renders it on that port's rule
|
||||
alone, as the subnets it resolved to. A machine that reports no private network has a `local-network`
|
||||
port admitted from no local network, and the rule set and the plan say so. The plan shows what each
|
||||
such port resolved to, and `node` shows which attached networks counted and why the others did not.
|
||||
A laptop that travels admits whichever local network it is on, which the plan makes visible.
|
||||
|
||||
*2026-10-02.* **The proxy serves an internal name to the private network only**
|
||||
(ADR 0138,
|
||||
|
||||
@@ -5,7 +5,7 @@ code:
|
||||
- mesh-host cmd/mesh-bootstrap
|
||||
- mesh-host internal/bootstrap
|
||||
- mesh-lab test/integration/whole-mesh-full.test.ts
|
||||
updated: 2026-09-22
|
||||
updated: 2026-10-13
|
||||
decisions:
|
||||
- "ADR 0100"
|
||||
- "ADR 0101"
|
||||
@@ -16,6 +16,8 @@ decisions:
|
||||
- "ADR 0005"
|
||||
- "ADR 0010"
|
||||
- "ADR 0036"
|
||||
- "ADR 0336"
|
||||
- "ADR 0338"
|
||||
---
|
||||
|
||||
# Raising a mesh
|
||||
@@ -145,6 +147,38 @@ plus the one thing only a builder needs. The account is issued before the machin
|
||||
because a builder that arrives without its credential starts, finds nothing it may read, and waits,
|
||||
which looks exactly like a builder with no work.
|
||||
|
||||
**The builder is assigned before the package registry exists, and is handed the forge genesis raised**
|
||||
(ADR 0336). The builder's module, `build-agent`, requires the package registry
|
||||
(`npm-package-registry`) only optionally (ADR 0330), so assigning it on a mesh where nothing provides one is
|
||||
not refused: the requirement is listed unbound by `plan` and `node`, and binds with nobody's act on the
|
||||
composition after the gitea module is assigned. Genesis is not reordered for it. The forge raised at the
|
||||
packages step is a plain container, not the gitea module, and provides nothing, and the gitea module needs
|
||||
the store, the vault and a route, which come after the base.
|
||||
|
||||
So the packages step, once it has raised the forge and made its account `mesh-builder`, seals the builder on
|
||||
this machine one own-secret, the **genesis document** (`genesis-package-registry`): the forge's address, the
|
||||
port this machine gave the package registry, the npm path, and the account with its password. The manifest
|
||||
declares it as issued outside the mesh, so no other machine is ever given one. The sdk step publishes the
|
||||
SDK with that account and the base step resolves it with the same one. Nothing else in genesis needs the
|
||||
registry.
|
||||
|
||||
A real binding wins over the genesis document whenever both are present, and every build's log says which of
|
||||
the two it resolved from. A build that needs the registry (a `package` artifact, an image whose Dockerfile
|
||||
names `.npmrc`, a TypeScript artifact with an `@novox/` dependency) on a builder that has neither fails before
|
||||
anything is built or published. Its outcome names the machine, says the requirement is unbound there, and
|
||||
names the gitea module as what provides it. It never builds without the credential and never resolves the
|
||||
mesh's scope from the public registry. Go artifacts, images that do not name `.npmrc` and upstream images
|
||||
need no registry. The gitea module's own server is an upstream image, so building the forge needs no forge.
|
||||
|
||||
**The store genesis raises is the `postgres` module's server, so adopting it keeps it** (ADR 0338, the
|
||||
store's twin of the bus's ADR 0333). The bundle raises the store under the name, on the image, in the data
|
||||
directory and with the superuser password file that the catalogue's `postgres` module declares, and every step
|
||||
of the bundle and of the installer reaches it by that name. At the store step the module declares the
|
||||
container the bundle raised, so the store is adopted where it runs and is not restarted. The module does not
|
||||
change, so no running mesh's store moves. *How it is checked:* a test reads the module's manifest and holds
|
||||
the bundle to it, the module's image is measured initialising a new store on a data directory made as the
|
||||
bundle makes it, and the bed that raises genesis from bare passes the store step and reaches the broker step.
|
||||
|
||||
**The core modules are built.** Each is named by a repository and a path
|
||||
(ADR 0069), and the builder is
|
||||
asked for each in turn: it clones, reads the manifest at that path, produces what it declares,
|
||||
@@ -226,6 +260,11 @@ is not yet wired is the genesis step that raises gitea and publishes the SDK ahe
|
||||
the toolchain's own manifest still names the SDK by a git URL. Until both land, the base build clones
|
||||
the SDK inside `docker build`, which is slow and names a branch head rather than a version.
|
||||
|
||||
**The genesis credential outlives the binding.** Once the gitea module is assigned the builder resolves the
|
||||
registry from its binding, but the genesis document and the `mesh-builder` account are still there, so the
|
||||
mesh holds two credentials for one registry. Withdrawing them is not decided; the
|
||||
issue tracker holds it as issue 602.
|
||||
|
||||
## How these rules are checked
|
||||
|
||||
| Rule | Checked by |
|
||||
@@ -236,6 +275,7 @@ the SDK inside `docker build`, which is slow and names a branch head rather than
|
||||
| An image is named exactly | A machine refuses a bundle naming an image by tag. The refusal is exercised, not assumed. |
|
||||
| The installer is what installed this | **Nothing.** See above. |
|
||||
| The builder can arrive on a fresh mesh | The installer carries it and installs it as its last step, and the genesis bed raises a machine by running the installer. A bed that raises one any other way fails its own acceptance check. |
|
||||
| The builder is assigned before the package registry exists | The build-agent manifest's own test holds the requirement optional and the genesis document declared and issued outside the mesh; the controller's resolution test over the real catalogue assigns the builder with no provider, lists the requirement unbound, and binds it once gitea is offered. The builder's tests refuse a package, an `.npmrc` image and an `@novox/` TypeScript build with neither binding nor genesis document, before anything is built, and build a Go artifact; they read the genesis document only without a binding. The installer's test seals the document at the packages step and gives the builder no `serves` setting and no `npm-password`. **Not yet proved live:** a genesis bed from bare passing the builder, packages, sdk and base steps. |
|
||||
| The controller a mesh runs is one it built | The genesis bed asserts the running controller is pinned to a digest this mesh's own registry serves, for an image built from a named repository and commit — not one the installer carried. |
|
||||
| A core module is built rather than only carried | The controller is rebuilt from its own repository and path, and the running mesh is upgraded to the result — the same path any module takes. |
|
||||
| Installing produced a mesh that can produce | A module with source of its own is asked for and comes back pinned to a digest this mesh's registry assigned, not to a placeholder. **Done by hand on a raised machine, not yet by a bed** — it built the shared base and then a module naming that base. Nothing automated asserts it, which makes this the weakest check on this page. |
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-controller internal/catalogue]
|
||||
updated: 2026-10-12
|
||||
updated: 2026-10-13
|
||||
decisions:
|
||||
- "ADR 0346"
|
||||
- "ADR 0312"
|
||||
- "ADR 0319"
|
||||
- "ADR 0309"
|
||||
@@ -278,6 +279,21 @@ rather than recomputing it. A consumer that writes the derived value into its ow
|
||||
of asking for it is refused, naming the placeholder to use. *How it is checked:* the unit tests in
|
||||
ADR 0202's "how this is checked", each run against the unchanged controller first.
|
||||
|
||||
*A provider may serve its own composed names (2026-10-13, ADR 0346, issue 624):* a served value may
|
||||
also name what the mesh composed for the **provider** through a requirement it contributes to, in the
|
||||
same words its own files use: `${bound:<requirement>:name}`, `internal-name`, and `name-<local>` or
|
||||
`internal-name-<local>` for a contribution under a local name. The forge serves the package registry's
|
||||
public root as `https://${bound:route:name-web}/api/packages/novox/npm/`, so a consumer learns the
|
||||
registry's public name from its binding rather than from a second copy kept by hand. The mesh fills it
|
||||
from the provider machine's own composition on both paths, a provider on another machine and one on
|
||||
the consumer's own, so the binding and the proxy name one host. Where the provider's machine composes
|
||||
no such name (it has no public domain, or the endpoint is not reached publicly), the key is left out of
|
||||
what the consumer is told, and `plan` says so on the provider's machine and under the consumer's need.
|
||||
*How it is checked:* the module check refuses a served value naming a requirement the module does not
|
||||
contribute to or a fact other than those names, one with a default (`|default:`), and one nested in a
|
||||
list or an object; the controller's unit tests for both paths and for the
|
||||
left-out key, each run against the unchanged controller first.
|
||||
|
||||
## How a definition reads what was resolved
|
||||
|
||||
**One form, naming a requirement and a field of its contract.** A definition that needs the database's
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-catalog modules/claude-code]
|
||||
updated: 2026-10-10
|
||||
updated: 2026-10-13
|
||||
decisions:
|
||||
- "ADR 0343"
|
||||
- "ADR 0274"
|
||||
- "ADR 0273"
|
||||
- "ADR 0266"
|
||||
@@ -402,7 +403,7 @@ the helper of §9 where that is an agent account):
|
||||
"Completely empty", the operator's words, means: nothing in the home was put there by hand. What is not the
|
||||
module's rendering is the program's own working state, excluded by name in the module's documentation and
|
||||
never written or removed: the credentials file (§5), the history, the sessions and their environments, the
|
||||
projects and their memory, `todos/`, `plans/`, `statsig/`, the shell snapshots, the file history, the debug,
|
||||
projects and their memory (each project's memory folder is also carried between nodes since ADR 0343; see §12), `todos/`, `plans/`, `statsig/`, the shell snapshots, the file history, the debug,
|
||||
cache, paste and telemetry directories, the statistics, the daemon's files (`daemon-auth-cooldown` among
|
||||
them), the dot-markers (`.last-cleanup`), `skills/synced/`, and the backups, worktrees, jobs, feedback, state
|
||||
and IDE directories. **Two are the program's and watched**: `settings.local.json` (the permission answers a
|
||||
@@ -520,6 +521,35 @@ A node away for more than the 30 days the router keeps its record, or joining la
|
||||
approval and does not render it; the list says so. The module uses the `operator-channel` seat, which grants it the
|
||||
asker's subjects and its own records there.
|
||||
|
||||
## 12. A session's memory follows the operator
|
||||
|
||||
*Added 2026-10-13 by ADR 0343 (issue 367).*
|
||||
|
||||
A session started on another node starts with what the sessions on every other node learned. **The module
|
||||
carries each project's memory folder**, `~/.claude/projects/<project>/memory/`, and nothing else of the home. A project is
|
||||
one entry of the projects directory and a path is relative, no part of it empty, `.` or `..`: a value in the state
|
||||
naming anything else is never taken, and nothing is touched for it.
|
||||
|
||||
| Step | What happens |
|
||||
|---|---|
|
||||
| pick up | the module's own long-running process on each node looks at the memory folders at least every ten seconds; a file whose text differs from the version last carried is put in the module's kept `memory` state, one key per home kind and file: the project, the path, the text, its digest, the digest of the version it replaced, the node and when it changed |
|
||||
| carry | every node watches the `memory` state (the whole set at start, then each change) and writes an incoming version where its own file holds no change it did not carry (the version replaced, the version it last carried, or none); staged and renamed in, never through a symbolic link, the replaced version kept in `memory-copies/` in the module's state directory for 30 days |
|
||||
| conflict | a file changed on two nodes since the version last carried becomes one file holding both, the newer first, then a line naming each version's node and time, then the older (two versions too large for one file: the newer in it, the older beside it as `<name>.older-from-<node>`); what was written is carried, so every node ends on it, and the operator is told by a warning through the operator channel's `notify`, cleared on the file's next change without a conflict |
|
||||
| remove | a carried file removed from a memory folder that still exists is put in the state as a removal marker for 30 days; a node still holding the removed version moves it into `memory-copies/`, a node that changed it keeps and carries it again; a missing folder or an absent key removes nothing |
|
||||
| first carrying | a node with no record of what it carried takes an equal file as agreed and a different one as a conflict, so every version learned before is kept |
|
||||
| list | `claude_code_memory`: what this node carries, each file's fingerprint and last version, what was not carried and why, open conflicts and copies; never the text |
|
||||
|
||||
**What is not carried**: a symbolic link, a file over 128 KiB or too large for one value of the state once encoded, and a file holding a key block, a signed
|
||||
token, a known token prefix or a password in an address. Each is listed with why.
|
||||
|
||||
**Homes of one kind only.** Memory moves among the homes where the agent runs as the operator's account,
|
||||
and separately among the homes of an agent account of its own (§9); never from one kind to the other, so a
|
||||
confined agent's text never reaches a session run as the operator (issue 340).
|
||||
|
||||
**No tool writes memory.** The `memory` state and the folders are written only by the module's own process,
|
||||
from what the module on another node carried; issue 340's boundary holds. A value put straight into the
|
||||
state by a holder of a tool runner's credential is not yet told apart (issue 638).
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Check | Defends |
|
||||
@@ -547,6 +577,8 @@ asker's subjects and its own records there.
|
||||
| live: a proposal for every node shows on the operator's phone with Approve and Decline; after Approve a new session on another node reads it | ADR 0274 |
|
||||
| live: a skill registered at the mesh scope is offered as `nox-mesh:<name>` in a new session on each machine | ADR 0216 |
|
||||
| the module's test: the "instead of" table is generated from the controller's answers, ordered and cut; the guard refuses ssh to a mesh machine by name, domain, internal name, address, alias and jump, and the local work-arounds for a mesh name, naming the tool — and lets git to the forge, ssh beyond the mesh and a command merely mentioning ssh through; the override is honoured only from the session's start and only when recorded | ADR 0245 |
|
||||
| the module's tests over temporary homes and a state in memory, never the live folders: a memory file written on one node appears unchanged on another; only `projects/*/memory/` is carried, and a symbolic link, an oversized file and a key block are not; homes of the two kinds never take each other's memory; a file changed on both ends as one file holding both, newer first, with a warning; a removal moves the other node's unchanged file into `memory-copies/` and a changed one is kept and carried again; a missing folder and an absent key remove nothing; a first carrying loses nothing | ADR 0343 |
|
||||
| live: a file written in a project's memory folder on the laptop is listed by `claude_code_memory` on the workstation with the same fingerprint within a minute; a file changed on both is kept twice in one file, with a warning on the operator's channel | ADR 0343 |
|
||||
| the mesh MCP server's provision resolves by co-location; a machine without the mesh MCP server refuses the module by name | ADR 0027, ADR 0152 |
|
||||
| the account the agent runs as: the facts fall back to the operator account; an agent account of its own is judged unable to become root, or raises `agent-can-become-root`; its files are written only through the helper; a session as another account is refused with no override | ADR 0266 |
|
||||
| a new session on the assigned workstation lists the mesh MCP server's five tools under `mesh` (ADR 0195) and answers "which node am I" from the instruction file | the exit of the build |
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-controller, mesh-tools, mesh-sdk, mesh-catalog]
|
||||
updated: 2026-10-12
|
||||
updated: 2026-10-13
|
||||
decisions:
|
||||
- "ADR 0347"
|
||||
- "ADR 0345"
|
||||
- "ADR 0344"
|
||||
- "ADR 0332"
|
||||
- "ADR 0308"
|
||||
- "ADR 0286"
|
||||
@@ -185,6 +188,25 @@ An id; the asker (bus principal and machine); the kind with its options or bound
|
||||
priority (urgent or normal); optionally a timeout and a default; optionally a conversation handle;
|
||||
optionally a group, for batching. The words pass the content rule.
|
||||
|
||||
*Amended 2026-10-13, ADR 0345 (issues 383 and 639):* **an ask reads at a glance.** Its words are a headline
|
||||
saying what it changes for the operator; an explanation that is a summary of two or three short sentences on
|
||||
one line, then at most three points; and Details — who proposed it, a fingerprint, how to read it whole. The
|
||||
router sends the headline as the title and the explanation alone as the body, and puts everything else in the
|
||||
message's note: who asked and when with what happens if nobody answers, what each answer does, where else it is
|
||||
answered, and the Details. A channel shows the note apart (Telegram: collapsed after a blank line; the desk:
|
||||
after the body). Answered, the message is one line, "You chose Approve at 16:06 on Telegram · <headline>". A
|
||||
decision that needs the operator carries a brief its ask shows in place of its clauses; a settings proposal is
|
||||
a summary and a point per key, at most three; an instructions proposal's text and a question's asker are their
|
||||
Details.
|
||||
|
||||
*Amended 2026-10-13, ADR 0347 (issue 383):* **every value an approval sets is in the note.** A settings
|
||||
proposal's Details list every key it sets with its value, one line each and never cut, the value masked where the
|
||||
content rule requires. Their whole form (`WholeDetails`) is shown in their place where the router shows the whole
|
||||
words, held to the secret rule alone: there the operator reads every value Approve sets, but a value shaped like a
|
||||
secret, which is withheld and named by its key. A point cut for the glance says so, and no more than the note
|
||||
holds. A channel that bounds a message's length cuts the body first and says where it cut; it cuts the note only
|
||||
when the title and the note alone do not fit, and says that too.
|
||||
|
||||
### Life
|
||||
|
||||
**open → answered | defaulted | expired | cancelled.**
|
||||
@@ -253,6 +275,7 @@ withdrawn from routing and reported until it passes. A new word is a new version
|
||||
| | `operator-first` | the operator can write unprompted | a simulated message yields a `message` envelope |
|
||||
| trusting | `verified-sender` | the answer came from the operator's own account, through a holder no agent shares | a choice from an identity not on the list is dropped and reported; placement checked |
|
||||
| | `exact-render` | the ask is shown as the router rendered it | rendered text equals the router's, byte for byte |
|
||||
| | | *Amended 2026-10-13, ADR 0345:* a channel may format the router's words (Telegram: the title bold, a key line in monospace, the note in a collapsed blockquote) and escapes their markup; it never rewords them | the words, formatting and runs of whitespace aside, equal the router's — what a tap on the message is compared by |
|
||||
| | `code-factor` | a typed code reaches the router by request and reply, never judged by the holder | a code never appears in an event; it is deleted from the conversation where the service allows |
|
||||
| | `key-factor` | a security key's assertion over the router's challenge reaches the router | the challenge is the router's; the signature is checked by the router |
|
||||
|
||||
@@ -353,6 +376,11 @@ is not exempted.
|
||||
- **A refusal is said to the sender** — the `notify` call or the ask is refused naming
|
||||
the offending part, so the asker can rephrase or move the specific into a reference — and raises
|
||||
`channel-refused`. The offending part is never sent to a channel.
|
||||
- *Amended 2026-10-13, ADR 0345 (issue 383):* **an ask may carry its explanation whole** (`Whole` beside
|
||||
`Explanation`), its paths and addresses as they are: what the operator approves must be readable where they
|
||||
approve it. The router shows the whole only on an ask's own message, on a kind declaring `verified-sender` that
|
||||
was given the ask's answers, and holds it to the secret rule alone; a secret's shape in it refuses the ask as a
|
||||
whole. The rule above holds for everything else: every condition's words, and every kind that proves nothing.
|
||||
|
||||
### References
|
||||
|
||||
@@ -431,11 +459,32 @@ the factors are the router's state.
|
||||
| approve or reject a waiting retirement set (`retire approve` / `retire reject`) | approve |
|
||||
| confirm a kept binding moves (`pin`, while `binding-kept` names it) | approve |
|
||||
| end a stuck plan; send a machine its declaration by hand; reset a bus consumer; retry a healer | approve |
|
||||
| set or clear a trusted setting an agent proposed (`settings propose`, ADR 0277) — bound to the exact values shown; the push stays a separate act | approve |
|
||||
| set or clear a trusted setting an agent proposed (`settings propose`, ADR 0277) — bound to the exact values shown; the one Approve also sends it (ADR 0344) | approve |
|
||||
| silence a condition | acknowledge |
|
||||
| delete a retired consumer's data, one or by age (`cleanup delete`) | destroy |
|
||||
| change the operator's identities, the away channel, or a factor's enrolment | destroy |
|
||||
|
||||
### An approved settings change takes effect (ADR 0344)
|
||||
|
||||
*2026-10-13, issue 641.* Approving a settings proposal is the act that makes it take effect. The
|
||||
serving controller sets the layer and, in the same act, opens a **settings walk**: every machine
|
||||
that runs the module and that the layer reaches, the control node last, each sent and its module
|
||||
judged at the first-node gate before the next. A settings walk sends the layer and no build: every
|
||||
module on the machine stays at the build it was last sent, so a build held for a person, or waiting
|
||||
for a first-node gate, stays where it is. A send that would move a running module's data stops the
|
||||
walk and names the `push --move` that sends it. The walk says its start and end like every walk, so
|
||||
its end is told as every walk's is. A failure stops the walk and leaves the layer as approved; the
|
||||
previous layer is in the settings history. The ask says all this before the Approve, and no longer
|
||||
names a push. A layer set by hand still waits for a push.
|
||||
|
||||
A delivered file is not yet an applied one: a module whose program reads its settings only when it
|
||||
starts applies them itself (a service's `restart-on`, or a tool of its own). Betterbird is such a
|
||||
module; issue 643 makes it restart Betterbird at a quiet moment.
|
||||
|
||||
How it is checked: the controller's tests open the walk on the warrant, send one machine at a time
|
||||
with every build kept and the module judged, stop before a data move and on a failure without
|
||||
holding other walks back, and read the ask's words.
|
||||
|
||||
### Proofs and tiers
|
||||
|
||||
- **P1, a verified sender:** a Telegram tap from the operator's own account, through a holder placed
|
||||
|
||||
@@ -4,6 +4,7 @@ status: in-progress
|
||||
code: [mesh-controller, mesh-catalog modules/mesh-delivery]
|
||||
updated: 2026-10-13
|
||||
decisions:
|
||||
- "ADR 0350"
|
||||
- "ADR 0335"
|
||||
- "ADR 0323"
|
||||
- "ADR 0313"
|
||||
@@ -58,7 +59,12 @@ commit's journey and in what order the steps are allowed.
|
||||
group, the state table and their state, and nothing else.
|
||||
- **The forge's holder** sets a commit's statuses, keeps the delivery's view as one comment on the pull
|
||||
request, and appends to the commit's note under `refs/notes/mesh-plan`.
|
||||
- **The build seat** builds and checks, asked by the controller as before.
|
||||
- **The build seat** builds and checks, asked by the controller as before. **A change is built once** (ADR 0350):
|
||||
a check that passes also builds what its merge would move into the artifact store's scratch namespace, under a
|
||||
reuse key (the source fingerprint and the builder's version), and the walk of the merge commit copies a build
|
||||
of the same key, which the check's verdict names, by digest instead of building. Otherwise the walk builds and
|
||||
says why, per module, in its record. Check builds go when the walk answering the merge ends, when the pull
|
||||
request closes unmerged, and after seven days.
|
||||
|
||||
## A delivery
|
||||
|
||||
@@ -133,6 +139,15 @@ delivery merged with no walk answering it within ten minutes is held too, with t
|
||||
says `covered: a later merge that contains it was delivered` as a success. A module that runs on one
|
||||
machine only is covered too: its walk records the empty rest as sent once its first machine passes
|
||||
(ADR 0270, as corrected 2026-10-12);
|
||||
- *a merge whose walk said its modules unchanged* moves nothing too, and is `delivered` when that walk ends,
|
||||
whichever commit the walk carried (issue 611). A delivery plan made while another walk built a module reads
|
||||
that module whole and names it; the walk, cut later, reads the build source that build recorded (ADR 0267) and
|
||||
may find none of the changed files in it. The walk's record then says so in `delivery.unchanged`: each such
|
||||
module with why, "its build source holds none of the changed files", and not in its modules, which keep
|
||||
meaning what the walk builds. A delivery whose every module the walk built or said unchanged is delivered,
|
||||
saying which matched what runs. One with a module the walk neither built nor said unchanged is read as below.
|
||||
Checked by the controller's `TestAWalkThatMovedNothingSaysWhichModulesMatchedWhatRuns` and mesh-delivery's
|
||||
`TestAMergeWhoseModulesMatchWhatRunsIsDelivered`;
|
||||
- *a stop whose walk ended without building its modules* (issue 474) stays `stopped`, a final state, and
|
||||
is read as covered once a later merge covers it by the same test: `mesh/delivery` then says `covered after
|
||||
its stop` with the covering merge, once, as a success. No state or transition is added (issue 480);
|
||||
|
||||
@@ -5,8 +5,11 @@ code:
|
||||
- mesh-controller internal/catalogue (the seats node-nfs-server and node-mounts)
|
||||
- mesh-catalog modules/nfs-server
|
||||
- mesh-catalog modules/mounts
|
||||
updated: 2026-10-10
|
||||
- mesh-catalog modules/smb-server (to be built)
|
||||
updated: 2026-10-13
|
||||
decisions:
|
||||
- "ADR 0341"
|
||||
- "ADR 0340"
|
||||
- "ADR 0283"
|
||||
- "ADR 0277"
|
||||
- "ADR 0263"
|
||||
@@ -233,9 +236,50 @@ separate decision.
|
||||
**Stop at once** when a share reads `failed` for a reason not understood, when a dry run would touch a
|
||||
line or a dataset other than the one asked for, or when anything under the library changes owner.
|
||||
|
||||
## 6. The home network (ADR 0341, after ADR 0340)
|
||||
|
||||
*2026-10-13.* The home server's SMB share and its NFS export to the home network are still used (the
|
||||
operator, on issue 613), so both become modules' and neither is retired. Both are reached from the
|
||||
home server's local network and the private network only, through the reach `local-network` of
|
||||
ADR 0340, and neither is built into use before that reach is accepted and built.
|
||||
|
||||
- **`smb-server`** holds the node seat `node-smb-server`, with the verbs `shares` (r), `sessions` (r),
|
||||
`test` (r), `reload` (a) and `adopt` (a). Its setting `shares` lists the shares (a name, the path,
|
||||
read-only or read-write) and its setting `users` the login names. It provides no provision: its
|
||||
clients are devices that are not nodes, and a node that wants the library mounts the NFS share.
|
||||
- **Identity.** Every client acts as the shared folder's owner (`force user`, `force group`, read from
|
||||
the folder after any symbolic link), and the refusals of section 1 hold for SMB shares too.
|
||||
- **Protocol.** SMB 2.1 and later on 445/tcp alone; NetBIOS is off, so 137, 138 and 139 stay closed
|
||||
and `nmb` is stopped once the module's configuration takes over.
|
||||
- **Passwords.** One secret per login, `smb-password-<user>`, a member of a family the mesh never makes
|
||||
a value for (ADR 0283). A login already in Samba's password database whose secret is not given keeps
|
||||
its password, said as kept as found.
|
||||
- **The configuration file.** Once adopted, the module writes it whole from its settings and checks it
|
||||
with Samba's own checker before it reloads. `adopt` shows each hand-written share beside its
|
||||
rendering, refuses while one has no counterpart in the settings, and keeps a dated copy.
|
||||
- **NFS to the home network.** `nfs-server`'s `grants` may name `local-network` for a share, beside
|
||||
node names. The export line names the subnets the home server's local network resolved to, never a
|
||||
range typed in a setting, with `all_squash` and `mp` as in section 1. The module refuses that grant
|
||||
while port 2049 is not bound with the reach `local-network`, and says so.
|
||||
- **The ZFS property** is cleared by `node-nfs-server.adopt` only once the module's own export covers
|
||||
every client the property exported to, so for the home network only after the module's
|
||||
`local-network` export is in the kernel's table. ZFS's own file in `/etc/exports.d` goes with the
|
||||
property. This replaces step 6 of section 5 for a property that exports to the home network.
|
||||
- **No jail.** The mesh's jails never ban a private address (ADR 0186), so a jail on 445 could ban
|
||||
nobody.
|
||||
- **No data moves.** The library, its owner and modes, its dataset, and Samba's password database
|
||||
stay where they are.
|
||||
|
||||
Taking it into use, in order: the two modules are built and the home server's `smb-server` assignment
|
||||
is made with its settings reproducing the hand-written share; `adopt`'s dry run is read; the operator
|
||||
confirms it; a device on the home network lists the library over SMB; the NFS grant to `local-network`
|
||||
is set and a device mounts it with NFS version 4; then the ZFS property is adopted. Each step's undo
|
||||
is the one before it: unassign, the dated copy, the old property value set by a person.
|
||||
|
||||
## Not in this design
|
||||
|
||||
- **Samba.** Kept as found. An `smb-share` seat follows when a node needs it.
|
||||
- **Clients on the underlay.** NFS for devices that are not nodes waits on issue 154.
|
||||
- **Finding a share by announcement.** A device finds the SMB share by the home server's name or
|
||||
address; announcing it over mDNS waits on issue 154.
|
||||
- **NFS version 3 for devices on the home network.** The server serves version 4 only (section 1).
|
||||
- **Mounting when a device appears.** Waits on a local-link reach (issue 154), because the packet
|
||||
filter drops mDNS. The automount already mounts on the next access.
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-10-12
|
||||
updated: 2026-10-13
|
||||
decisions:
|
||||
- "ADR 0316"
|
||||
- "ADR 0317"
|
||||
- "ADR 0326"
|
||||
- "ADR 0329"
|
||||
---
|
||||
|
||||
# 56 — The mail client, set up by the mesh
|
||||
@@ -14,12 +16,17 @@ decisions:
|
||||
upstream release, ships the Owl add-on with it through Betterbird's enterprise policy, and declares the
|
||||
operator's mail accounts without their credentials. A password or token never passes through the mesh: each
|
||||
account signs in once, at a person's first use, and Betterbird keeps the result in its own login store.**
|
||||
(ADR 0316, from issue 345.)
|
||||
(ADR 0316, from issue 345.) **The module owns Betterbird's whole configuration as settings, mesh-wide with a
|
||||
node's layer over each: the preferences, the accounts, the identities with their signatures, the CalDAV
|
||||
calendars, the CardDAV address books, the filters and the tags; its tools show the operator's own values, never
|
||||
a credential.** (ADR 0329, from issue 586.)
|
||||
|
||||
**Acceptance.** On a desktop node set up from nothing, the operator starts Betterbird from the launcher and
|
||||
finds Owl installed and the declared accounts listed; each asks for its sign-in once, and the work account is
|
||||
created in Owl's own setup. On a desktop node that ran the community package before, the same Betterbird opens
|
||||
the same profile, with its mail and logins, after the switch.
|
||||
the same profile, with its mail and logins, after the switch. Once the operator has approved the derived layers,
|
||||
every desktop node shows the same signatures, calendars, address books, filters and tags, and `betterbird_drift`
|
||||
on each says no difference.
|
||||
|
||||
The module's look-only part (status, preferences, a declared user.js, its log, a restart and a reminder test)
|
||||
is issue 345's and is not repeated here. This document adds what sets Betterbird up.
|
||||
@@ -31,11 +38,13 @@ is issue 345's and is not repeated here. This document adds what sets Betterbird
|
||||
| Betterbird, the upstream release archive, unpacked | the installation directory the community package uses | owned, whole |
|
||||
| the enterprise policy, `policies.json` | the installation's `distribution` directory | owned, whole |
|
||||
| the Owl add-on, the vendor's xpi | the installation directory | owned, whole |
|
||||
| a defaults file naming the system's spelling dictionaries | the installation's `defaults/pref` | owned, whole |
|
||||
| a defaults file naming the system's spelling dictionaries, and one naming the preferences kept at Betterbird's own default (no value of the operator's) | the installation's `defaults/pref` | owned, whole |
|
||||
| a launcher entry and a start script | under the system's local prefix | owned, whole |
|
||||
| the user.js, from `prefs` and `accounts` | the module's directory in the home, copied into the profile by its tools | owned, whole |
|
||||
| the account list in the profile's `prefs.js` | the profile | written into: the module's own keys only |
|
||||
| the profile: mail, address books, calendars, logins | the profile | found |
|
||||
| the settings as the node-engine merges them, one mergeable file per group, each naming the keys it takes (ADR 0326) | the module's directory in the home | owned, whole |
|
||||
| the user.js, composed from the settings | the profile, placed by the tools | the module's, or never touched |
|
||||
| the lists in the profile's `prefs.js`: accounts, outgoing servers, an adopted account's identities, the calendars' order | the profile | written into: the module's own keys only, only while Betterbird is closed |
|
||||
| each declared account's filter file | the account's directory in the profile | owned, whole, only while Betterbird is closed |
|
||||
| the profile: mail, calendar events, contacts, logins | the profile | found |
|
||||
|
||||
The module creates no link: where the community package linked the command, the icons and the dictionaries,
|
||||
the module places a start script, a launcher entry that names the installation directly, and the dictionaries'
|
||||
@@ -159,14 +168,26 @@ the provider's own sign-in can issue. With no credential to carry, the rule that
|
||||
|
||||
## 6. What the tools say
|
||||
|
||||
`betterbird_status` and `betterbird_check` add, never with an address, a host or a name:
|
||||
**The tools show the operator's own configuration, never a credential** (ADR 0329 §1–§3, changing ADR 0316 §16
|
||||
and ADR 0317 §3). An answer may hold an account's name, address, user name and its servers' hosts and ports; an
|
||||
identity's name, address, reply-to, organisation and signature; a calendar's or an address book's name, address,
|
||||
colour and options; a filter's conditions and actions; a tag's name and colour. These values appear only in a
|
||||
tool's answer to its caller and, for a proposed value, in the controller's proposal record; never in an event, a
|
||||
log line, a condition, a metric, the module's record per node, an issue, the knowledge base or this repository.
|
||||
The operator channel's content rule does not apply to these answers, because it withholds the addresses they
|
||||
show; in its place every answer passes the module's own credential check, which withholds a field whose name says
|
||||
password, token, secret or cookie, and the password part of any address. The login store is read only to count,
|
||||
by server, the sign-ins kept. The values reach whoever may call the module's tools, which is every login and
|
||||
agent session on the operator's nodes; the ask itself still shows a proposed value in its shape.
|
||||
|
||||
- the installed version against the pin and the newest upstream release, and whether it came from the mesh or
|
||||
from the community package;
|
||||
- whether the policy file is placed;
|
||||
- Owl's version against its pin, and whether it is active;
|
||||
- how many declared accounts are in the profile's list, beside how many other accounts exist, and how many have
|
||||
not signed in yet.
|
||||
| tool | answers |
|
||||
|---|---|
|
||||
| `betterbird_status`, `betterbird_check` | the installed version against the pin and the newest release; the policy placed; Owl's version and whether it is active; the declared accounts in the list and how many have not signed in yet; and, per kind, the declared entries that drift or are refused and the changes waiting for Betterbird to close |
|
||||
| `betterbird_accounts` | the accounts with their values and the declared account each is, Owl's marked by name and address; with `propose`, `accounts` derived and proposed, the value shown |
|
||||
| `betterbird_identities`, `betterbird_calendars`, `betterbird_address_books`, `betterbird_filters`, `betterbird_tags` | the profile's entries beside the declared ones, how each resolved, an ICS calendar by its name only |
|
||||
| `betterbird_derive` | the five kinds derived per layer, the fields another node holds differently, what was left out; with `propose`, the next layer proposed |
|
||||
| `betterbird_drift` | per kind and declared entry: holds, differs (each field with both values), lacks, or refused; and the undeclared entries |
|
||||
| `betterbird_config` | each managed preference through the locks, the setting `prefs` and Betterbird's defaults, with its drift |
|
||||
|
||||
## 7. What changes for the builder
|
||||
|
||||
@@ -178,12 +199,79 @@ Not decided here: a package repository of the mesh's own, which would also answe
|
||||
node runs from outside the official repositories. ADR 0316 weighed it, and rejected it as larger than this
|
||||
module needs.
|
||||
|
||||
## 8. The rest of the configuration (ADR 0329)
|
||||
|
||||
**The preferences are one entry each.** The setting `prefs` maps a preference's name to its value, so a node's
|
||||
layer overrides one preference, or takes one out with null, and the operator's setup lives in the mesh-wide
|
||||
layer, not in the catalogue. The module ships only its policy's locks and the names it keeps at Betterbird's own
|
||||
default (issue 586). `prefs` refuses a credential's preference, an account's (§4), and the four prefixes the
|
||||
settings below own: `calendar.registry.`, `calendar.list.`, `ldap_2.servers.` and `mailnews.tags.`, so each value
|
||||
has one owner.
|
||||
|
||||
**Five more settings**, set mesh-wide so every desktop node has the same setup, with a node's layer holding only a
|
||||
difference that node is meant to have:
|
||||
|
||||
| setting | an entry | matched in the profile by |
|
||||
|---|---|---|
|
||||
| `identities` | an identity of a declared account: name, address, reply-to, organisation, the signature's text, whether it is HTML, whether it goes above the quote; the account's first identity takes its name and address from `accounts` | the account's default, or an identity of that account with the address |
|
||||
| `calendars` | a CalDAV calendar: name, address, colour, read-only, reminders, user name, the identity that answers invitations | its address |
|
||||
| `address-books` | a CardDAV address book: name, address, user name | its address |
|
||||
| `filters` | per declared account, the ordered list of filters: name, on or off, when it runs, all or any condition, conditions, actions, a folder named by its path within the account | owned whole |
|
||||
| `tags` | per tag key: name, colour, place | its key |
|
||||
|
||||
An identity, a calendar or an address book may carry, in a node's own layer only, the optional `key` of the
|
||||
profile's entry to adopt there, as `accounts` does. The first four are **trusted**, because each names where a
|
||||
password, a reply or forwarded mail goes; `tags` names no server and sends no mail, and is set through the
|
||||
`settings` verb as well. A setting holds no line break, so a signature is a list of its lines; no setting holds a
|
||||
password, a token or a cookie, and an address whose user part carries a password is refused.
|
||||
|
||||
**Adopted as accounts are.** At every apply each declared entry is matched as §4 matches an account: recorded,
|
||||
the module's own key, a node's `key`, else one match adopted, none created under the module's key, several
|
||||
refused with the remedy. Its declared fields go into the user.js under that key. An entry leaving the setting is
|
||||
released if adopted; if created, its preferences are removed, never a calendar's events, an address book's
|
||||
contacts or a cache. An entry no setting declares stays the person's. Owl's accounts and calendars, a calendar
|
||||
subscribed by an ICS address (such an address is often itself the secret), local calendars and local address
|
||||
books are never declared.
|
||||
|
||||
**Filters are owned whole per declared account.** For an account named in `filters`, the module writes its filter
|
||||
file from the declared list, turning each folder's path into that folder's address on the node; a filter added in
|
||||
Betterbird to that account is gone at the next apply, and drift shows it first. Before its first write on a node
|
||||
the module keeps the file it replaces in its own directory in the home. When an account leaves `filters`, the
|
||||
module stops writing its file, which stays the person's as last written, and says where the kept copy is. An
|
||||
account never named keeps its file.
|
||||
|
||||
**What Betterbird rewrites is written only while it is closed:** the lists in `prefs.js` and each filter file.
|
||||
An apply while it runs writes neither, and the module's check counts, per kind, the changes that wait; the next
|
||||
start from the launcher writes them. The user.js is placed as before, since Betterbird reads it only at its start.
|
||||
|
||||
**Derived from the profiles, proposed one layer at a time.** `betterbird_derive` asks every desktop node that
|
||||
holds Betterbird for what its profile holds. An entry found on every such node goes into the mesh-wide layer
|
||||
with the value of the node the derive was called on, and the answer lists every field another node holds
|
||||
differently, which then shows as drift on that node; an entry found on some nodes only goes into those nodes'
|
||||
own layers. With `propose` it proposes the next layer for the operator's Approve: first the mesh-wide one, then
|
||||
each node's that needs one, each only after the one before it was set or declined, against the layer as it then
|
||||
stands and carrying that layer whole (ADR 0277 keeps one open ask per module and machine). It proposes nothing
|
||||
while a desktop node does not answer, because a layer derived without it would put on every node what only some
|
||||
hold. The first mesh-wide proposal also carries the operator's setup of `prefs`, which the catalogue held before.
|
||||
|
||||
## How it is checked
|
||||
|
||||
- **The catalogue's tests:** both pins carry a URL, a version and a sha256; a Betterbird pin outside Owl's range
|
||||
is refused; the generated policy file names Owl as `force_installed` from the placed file with
|
||||
`updates_disabled`, and sets `DisableAppUpdate`; `prefs` refuses the account prefixes; `accounts` refuses a
|
||||
password-like field; a declared account's user.js holds only the module's own keys.
|
||||
`updates_disabled`, and sets `DisableAppUpdate`; `prefs` refuses the account prefixes and the four of §8;
|
||||
`accounts` refuses a password-like field; a declared account's user.js holds only the module's own keys; every
|
||||
mergeable file names the keys it takes, `tags` alone untrusted; the module's defaults hold no value of the
|
||||
operator's.
|
||||
- **The controller's tests:** a key of a trusted file that names its keys is refused through the `settings` verb,
|
||||
and a key of a file nothing trusts, `tags`, is set through it.
|
||||
- **The configuration's tests, on profile fixtures:** each kind adopted, created, or refused when ambiguous, a
|
||||
`key` settling it; leaving releases or removes and never touches events; a declared filter list, forwarding
|
||||
included, written with this node's folder addresses, the replaced file kept, a file never named left byte for
|
||||
byte, one taken out left as last written; with Betterbird running nothing of `prefs.js` or a filter file is
|
||||
written and the waiting changes are counted; derived from two nodes, an entry both hold mesh-wide with the
|
||||
calling node's value and the other's differing fields listed, one on one node in its own layer, Owl's, ICS
|
||||
calendars and a passworded address left out, and one layer proposed at a time; drift names each differing field;
|
||||
no answer holds a credential.
|
||||
- **The module's tests:** writing into `prefs.js` adds and removes only the module's keys, keeps the others and
|
||||
their order, and is refused while Betterbird runs; the count of declared accounts without a kept sign-in is
|
||||
taken from the login store's server names, which are kept in the clear, without decrypting anything.
|
||||
@@ -196,9 +284,14 @@ module needs.
|
||||
the mesh, the policy placed, Owl at its pin and active, the profile in use the same as before the switch, and
|
||||
the declared accounts in the list with how many have not signed in yet; the package manager's list of foreign packages no longer names the
|
||||
community package.
|
||||
- **Live, after the operator's Approve of the derived layers:** on the node derived on, `betterbird_drift` shows no
|
||||
drift for the mesh-wide entries; on the other desktop node it first shows each field where it differs, and,
|
||||
after Betterbird is closed there and started again, none; `betterbird_check` on both says no drift and no
|
||||
waiting change.
|
||||
|
||||
## References
|
||||
|
||||
- ADR 0316, ADR 0317, ADR 0180, ADR 0182, ADR 0205, ADR 0208, ADR 0277.
|
||||
- Issue 345 (the reminder window, extended on 2026-10-12), issue 342, issue 339.
|
||||
- ADR 0316, ADR 0317, ADR 0326, ADR 0329, ADR 0180, ADR 0182, ADR 0205, ADR 0208, ADR 0277.
|
||||
- Issue 345 (the reminder window, extended on 2026-10-12), issue 586 (the whole configuration), issue 342,
|
||||
issue 339.
|
||||
- Thunderbird's enterprise policy templates (ESR 153): `ExtensionSettings`, `DisableAppUpdate`, `Preferences`.
|
||||
|
||||
Reference in New Issue
Block a user