Files
hq/03-DESIGN/01-to-be/32-what-a-module-declares.md
T
jschoubben aac8494ca3 Say manifest validator, not judge: the operator asked why a new word was needed for the indexer's rules
The glossary gets the entry in the Delivery domain with the records that decide it, and retires the noun in
hq's prose on a Not line; the verb stays. Where "judge" named something else, the design now uses that
thing's word: the node-engine's look at health, an account or the network; the composition check built from
a change; the claude-code module's judgement tool. Five issue narratives keep the word of their day.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 23:32:13 +01:00

59 KiB
Raw Blame History

layer, status, code, updated, decisions
layer status code updated decisions
to-be in-progress
mesh-controller internal/catalogue/declaration.go
mesh-controller internal/catalogue/manifest.go
mesh-controller internal/link/serve.go
mesh-controller internal/link/bus.go
mesh-controller internal/broker/nats.go
mesh-controller internal/inventory/nodes.go
mesh-host internal/apply/apply.go
mesh-tools src/main.ts
mesh-catalog modules/mesh-catalog
mesh-tools node-tools/internal/runtime (a module's state, ADR 0201)
mesh-sdk (the typed bus-state entry in `requires` and its binding, ADR 0595)
2026-11-04
ADR 0609
ADR 0612
ADR 0613
ADR 0618
ADR 0606
ADR 0595
ADR 0596
ADR 0307
ADR 0240
ADR 0233
ADR 0201
ADR 0160
ADR 0126
ADR 0131
ADR 0128
ADR 0106
ADR 0041
ADR 0112
ADR 0083
ADR 0129
ADR 0135
ADR 0136
ADR 0187
ADR 0134

32. What a module declares, and what the bus makes of it

A module that speaks to the mesh requires the bus, and receives what it needs to connect (ADR 0128). What a module declares are relationships; subjects, streams, consumers and permissions are all derived from those, and a manifest never contains one.

Revised 2026-09-26. This document opened by calling the bus ambient — "no module requires it, the way no module requires a filesystem". Two counts say otherwise: of 72 modules in the catalogue, 49 take a broker credential and 23 do not, so an ambient connection would mint an account for a third of the catalogue that never speaks; and the 49 each hand-write the path it lands at, which is provisioning done badly by hand. The bus is required, and a module that does not require it has no account at all.

The requirement delivers the connection; the declarations shape the authority. requires: mesh-bus says this module talks to the mesh and grants no subject by itself. emits, consumes, tools, uses and a declared seat say what it may say and hear. Declaring a subject without requiring the bus is incoherent and refused at registration.

This document is the declaration model. Design 25 is the bus itself — subjects, streams, accounts, enrolment — and stays the authority on the wire. Design 19 is the specification an SDK implements, and is rewritten onto this in step 3 of ADR 0116.

1. A module names locally; the mesh derives the subject

This is the load-bearing rule. ADR 0112 says a manifest names no node, mesh or path. A transport address is the same class of thing: if manifests held literal subjects, reorganising the subject space would mean editing every module in the catalog, and the mesh would have hundreds of copies of a decision it made once.

declared derived
emits: order.placed publish on mesh.mod.<module>.event.order.placed
consumes: billing.order.placed durable consumer on mesh.mod.billing.event.order.placed
tools: status queue-group subscription on mesh.mod.<module>.tool.status
seat telegram-sender, accepts: send work-queue consumer on mesh.seat.telegram-sender.accept.send
uses: telegram-sender publish on that seat's accept subjects, and nothing else
state: servers a key-value bucket for the module, created by the controller; its instances write and read it
reads: billing.orders read and watch billing's orders bucket, and nothing else of it

Wildcards, and they are the mesh's rather than a bus's. Added 2026-09-27, from issue 127. A consumer may write * for one name and ** for the rest: *.download.completed is that event from any module, and ** on its own is every event in the mesh, which an audit logger wants and says in one token. Spelled this way rather than the wire's, for the reason everything else here is local — the bus the mesh runs on today spells these * and #, the one being built spells them * and >, and a manifest naming either would stop being true when the wire changed. An emitted event carries no wildcard: it names one event.

A module publishes under its own name, and an event about a role belongs to the seat. Added 2026-09-27, same source. The bus enforces that a namespace belongs to the module it is named for, so an event named for somebody else cannot be published at all. Where the event is really about a role — "the artifact store accepted an image" — the seat is the right home, because that name outlives whoever fills it, and a consumer written against the holder's own name breaks when the holder changes. Not yet possible in practice: seats carry protocol in the manifest and in the permission model, and the shared library has no way for a module to publish on one. Until it does, such an event lives under the emitting module's own name and the consumer carries that coupling.

How the rule is checked, because it was not. Added 2026-09-27, same source. Two checks, because the mistake happens at two scales. Per manifest, at registration: an event is a local name, and the old bus's form is refused with the name to write instead. Across the whole catalog, as a test: where a consumed event's emitter is present, it must emit that event. The second cannot demand a live emitter for everything — a module lives in its own repository and may be installed long before the one whose events it wants — so it says nothing about an absent emitter and everything about a present one. A subscription that matches nothing is not an error, it is silence, which is why nothing reported thirty-seven manifests being wrong the same way.

It is tools:, not serves:. Revision, found while implementing: the manifest already uses serves for the facts a consumer needs in order to reach a provision, and two meanings under one key in the file a module author reads most is a footgun. Worth noting that until now a module's tools were not declared at all — they were known only at runtime, from an environment variable in its image — so declaring them is new, and is what lets the mesh check that a module claiming a seat answers what that seat's protocol promises.

The event / tool / accept token is load-bearing, not decoration. Revision, found while defining the streams: a stream is defined by a subject filter, so a namespace holding both a module's events and its tool calls cannot be filtered into an events stream without capturing every tool invocation in the mesh — and a tool call must never be persisted (design 25 §3 keeps tools on core NATS, where a lost call is a timeout the caller already handles). The kind token is what makes mesh.mod.*.event.> a safe filter. The first draft of this table had no token, which reads better and cannot be implemented.

The test this must pass: the manifest survives the wire changing. Reorganise the subject space and every manifest in the catalog is still correct. That is the property ADR 0039 gave the sdk, applied to declarations.

A handler names an event the way its manifest does. Added 2026-09-27, found while fixing issue 127. The runtime handed a handler the event name alone, so a manifest declaring consumes: builder.built produced a pattern that could never match the key it was compared against, and a module consuming one event from two emitters could tell them apart only by reading a header. The subject already carries the emitter, so the key a module sees names it too — which makes a disagreement between a manifest and the code a typo rather than a category error.

2026-10-01 (ADR 0160): where a name lands is now issued to each assignment as a membership the controller publishes, rather than derived by a rule the runtime carries; a module still declares only names, and gains one fact about itself — whether its instances are interchangeable — which decides whether the mesh issues it the module's plain subject beside its machine's.

2. Three namespaces, and nothing else

Its own — mesh.mod.<module>.>. Its events and its tools. Nothing else may publish into it, so an event's source is a fact the bus enforces rather than a claim in the body.

Seats it holds — mesh.seat.<seat>.>. Full participation: consume what the seat accepts, publish what it emits, serve what it serves.

Seats it uses — publish only, and only on the accepts half. A sender cannot subscribe to a seat's inbound subject and watch other modules' traffic, and cannot publish the seat's outbound events and lie about outcomes.

A module naming anything outside these three is refused at registration. The whole permission set is derivable from the declaration; nobody writes an access rule.

3. Queues are derived, never declared

A module says what it reacts to, not how delivery works. Each consumes becomes one durable consumer; a seat's accepts becomes one work-queue consumer with a queue group named for the seat. The module does not name them, does not know their names, and cannot misconfigure them — and the controller stays the only writer of stream and consumer definitions (design 25 §3).

The mesh's own seats carry protocol too. Added 2026-09-27, ADR 0129. A seat declared by a module says what it accepts, emits and serves; the mesh-* set said only who does a job. So the mesh had roles it could not describe — a builder with three audiences for one outcome and no way to derive a grant for any of them, and an event genuinely about a role with nowhere to live but the namespace of whichever module happens to hold it. The mesh's seats now take the same three fields, and the same machinery derives the holder's authority, its work queue and its consumers.

So a build is work submitted to a role, like any other. The build seat accepts a build and emits an outcome, and the dedicated branch that carried builds retires: a work queue shared by several machines is exactly what accepts already is, and a second mechanism for it is two places a permission can be wrong.

And one publish reaches three audiences without anybody's inbox being opened. A build's outcome is the seat's own event: whoever asked matches it by the id their request carried, the controller records it, the catalog places it in the graph. That is the fan-out a shared exchange gave for free, written as a subject the mesh derived instead of a topology somebody configured — and it is why a holder needs no permission to publish into an asker's inbox, which is the one grant design 25 §4 refuses by name.

Retention belongs to whoever owns the namespace, not to a consumer. A seat declares how long its inbound backlog survives, because that is a property of the service:

seat: telegram-sender
  scope: mesh
  accepts: send        retain 7d
  emits:   delivered, failed
  serves:  status

If each consumer could tune it, the mesh's durability would be an emergent property of whichever manifest was edited last.

Why a module's own events do not carry their own retention, though the same rule would allow it. A seat owns its namespace and gets a stream of its own, so it can say. A module's events share one EVENTS stream, and three facts about JetStream decide that they must:

  • Storage is not a property of a subject. A subject is only an address; a stream is a separate object that captures subjects matching a filter. So "this topic is durable" is always really "some stream covers it", and something has to create that stream.
  • Overlapping streams are refused, not merged. Verified against the server: a per-module stream beside a shared mesh.mod.*.event.> is rejected with subjects overlap with an existing stream. So "one stream by default, its own for a module that wants different retention" is not available — it is all of one or all of the other, and a filter cannot express an exception either.
  • A stream per module breaks cross-module consumption. An audit logger consuming every module's events is one consumer on one stream today; with a stream each it becomes one consumer per module, created and destroyed as modules come and go.

So: one stream, and per-subject caps for the fairness that actually matters — a noisy emitter cannot evict a quiet one, which is verified (a cap of three, ten messages on one subject and one on another, leaves four). What is genuinely unavailable is a different age per module, because JetStream ages per stream and not per subject. A module that truly needs its own retention has a way to say so: declare a seat, which owns its namespace and gets its own stream.

Scope gives per-node workers without a new concept. A module running on three nodes that each need their own queue declares a node-scoped seat: one holder per node, three queues, same machinery. Mesh-scoped and node-scoped seats already exist; here they do the work of "one shared service" versus "one worker per machine".

4. Five relationships

provision event job state tool
shape 1:1 resource 1:many N:1 1:1 1:1
addressed to a provider the emitter's own namespace a seat one node a module or seat
who must act the provider nobody exactly one holder that node the server
credential sealed, per consumer none none none none
reply — none none, or an event later a report awaited
retention — age and size work queue, explicit ack last per subject none
declared provides/requires emits/consumes seat accepts / uses the mesh's own; a module's bus state in requires (ADR 0595), the old state / reads until it moves serves

Job is the one ADR 0041 had no room for. Its table has events at 1:many and provisions at 1:1; a module submitting work to a service is neither. It is not an event, because an event is a broadcast nobody is obliged to act on and a second holder would do the work twice. It is not a provision, because there is no resource and no credential. What makes it safe is not cleverness in the subscribe call but the seat: exactly one holder, so exactly one worker, by construction.

State is the shape the deploy path needs and nothing else uses. A declaration is not an event — replaying yesterday's is actively harmful — and not a job. Only the newest matters, which is last-per-subject retention, and a node that has seen sequence n refuses n−1 by construction. That is the wire-level answer to issue 107.

A module declares state too. Added 2026-10-04, ADR 0201. State was the mesh's alone, and modules had the same need with nowhere to put it: an MCP server registered for every machine, sent as an event, never reached a machine assigned afterwards — its consumer did not exist yet when the event passed — and a licence binding sent as events replays a week of rotations where only the latest matters. So a module names the state it owns with state, and another module's it reads with reads: <module>.<name>. Each is a key-value bucket the controller creates from the catalog, mesh-wide, existing from registration so a reader can watch before the owner runs anywhere (design 25 §3). Every instance of the owner writes; a reader reads and watches. A key may name a machine by the module's own convention; the mesh keeps one bucket per name, not one per machine, because "every server, for every machine" is then one list rather than a rollout.

What a module sees is what it named. Its assignment's membership lists its buckets by those names, with whether it may write, and the runtime answers get, put, delete, keys and watch for them on the bundle's channel — refusing, with the reason, a name it was not issued or a write to a bucket it only reads. A watch hands the current values first, then every change: a bundle that starts late, or starts again, has the whole of the state before it has any of the news.

A key's kept values are read too (2026-10-12, ADR 0307, extending ADR 0201). A bucket keeps as many past values per key as its owner declared, and the runtime serves them: history, given a state and a key, answers every value the bucket still keeps for that key, oldest first, each with its revision, whether it was a put or a delete, and when it was written; an empty answer for a key it holds nothing for. It is a read like get: served to the owner and to every reader, refused with the reason to a module the bucket was not issued to, and it writes nothing. The Go SDK's State(name) carries it as History(key), beside Get. Its first user is the issue tracker's switch to its log (ADR 0300 §6), which carries every value its bucket still keeps; that work is tracked by issue 502. How it is checked: the runtime's tests (several values and a delete answered oldest first, with revision, operation and time; an empty answer for an unknown key; a reader served, a module the bucket was not issued to refused, nothing written) and the SDK's tests of History.

The owner says how many past values a key keeps and how long a value lives, as a seat says how long its backlog survives (§3); the mesh caps a value's size and a bucket's. A bucket outlives its module's assignment — what a module stored is data (ADR 0030) — and one whose declaration is gone is reported, never removed by the mesh.

A module asks for its bus state in requires, and reads no other module's. Amended 2026-11-03, ADR 0595 and ADR 0596, from research 082. The state and reads of the two paragraphs above, and the sections logs, streams and objects beside them, are four vocabularies for one need: what a module keeps on the bus. They are replaced by one form, a provision of the bus module, as a database is postgres's:

  • An entry of requires, in its typed long form (ADR 0595 §1): the local name, the storage type, the template (state, events, work queue, log, objects, ledger) and the template's options, within their bounds; a ledger also names its type (its schema, its operations each with its ledger effect, its indexes). The templates, their fixed settings and their options are design 25 § 12.1. mesh-sdk's catalog package carries the entry's type and its strict decoding; every rule that judges it lives in the manifest validator (ADR 0595 §8).
  • The binding is what the module receives: the names on the bus for each local name, written into its membership by the grants part once the bus module says created (design 25 § 12.2). The module still names only locally (§ 1). A module whose binding is missing at start fails loudly, naming the entry.
  • One name, one form. A module asks for each entry once: an old section and the new form are never both used for one local name, and one name serves one storage type only.
  • No readers. The new form has no reads, and the manifest validator refuses one (ADR 0596 §2). Only the owner's account reaches its bus state. A module's events are subscribed to; its data is asked for: what another module needs of a module's kept data it asks through that module's tools or seat verbs (§ 4's tool relationship, serves), and what a module lets others hear is its declared events, heard through consumes (ADR 0363, unchanged; ADR 0596 §1). A bucket's layout is never a contract another module depends on; a tool or a declared event is.
  • A state may be read-only to the module's tools. A state's declaration may carry read-only: true: only the processes of the module whose bus.state names that state write it, and the module's tools read it, holding no publish on its subjects. The manifest validator refuses a read-only state that no process of the module names. How it is checked: the manifest validator's unit tests for the refusal, and the composition test that a tools bundle's account holds a read on the state and no write. The bus module's state spent (design 25 § 12.3) is the first user. (A design limit of the milestone's review; no record decides it yet.)
  • The move is a module at a time (ADR 0595 §11, ADR 0596 §3), each when it is next touched, keeping its local names and its old settings as options so its data stays. A reader moves after its owner: the owner declares the event or gains the tool its readers need first, and the manifest validator refuses a moved module's subscription to an event its owner does not declare. An old section is refused once no module but the foundation's uses it.

What follows for the paragraphs above: for a module that has moved, ADR 0201 §2 (another module's state read by naming it in reads) no longer holds, and the bucket "every instance of the owner writes; a reader reads and watches" becomes "every instance of the owner writes and reads". The runtime's get, put, delete, keys, watch and history stay the calls of the template state.

A module declares the data it holds. Added 2026-10-06, ADR 0233. State on the bus is one kind of data; the rest lives on a machine's disks or with a provider, and the mesh protects only what it is told about. One section, data, says all of it:

data. says
own each item the module keeps: id, path (${dir:<id>} or ${access:<id>}, or beneath one), class, and its protection — backup (copy, none, or {dump, into}) or redundancy with why that is enough — with optional measure (walk, dataset, shallow), within, active, why
consumers per provision it grants: the class of what it keeps for each consumer, and in — the own item, or the required provision, holding it
kept-by per provision it requires: the class of what of its own the provider keeps

The classes are irreplaceable, valuable, rebuildable and cache (and none, for consumers), ranked by the operator; the protections, the bindings that do not move, the backup holder's lines, what an unassignment retires and what the self-check raises are all derived from them, never written per module. The backup holder's composed backup and data lines are the bus-free half: a file the holder reads, like every contribution (ADR 0212). The rules are in the record; module check holds them.

A module says how each long-running resource is healthy. Added 2026-10-07, ADR 0240. A container or process that stays up, and a service stated running, carries a field named health: one kind of readiness check — the image's own adopted by name, HTTP or TCP to an endpoint named under listens, a command in the container, the unit's own readiness, or one of the module's tools — with its interval, timeout, failing looks, grace and the provision it exercises. Liveness needs no declaration. The node-engine runs every check; module check holds the bounds. The design is to-be 48.

5. Seats

A module declares a seat with its protocol, and the mesh enforces one holder at its scope (ADR 0126). A caller declares that it uses the seat, never the module, so the implementation can be replaced under it.

  • The set of seats is derived — the mesh's own, plus every registered module's — so it is both closed and extensible, and enumerating it is a query rather than an inventory.
  • mesh-* is reserved: the prefix is the reservation rule, and a module declaring one is refused at registration.
  • Two modules declaring the same name: the second is refused.
  • A module may not claim a seat whose protocol it does not implement.
  • Nobody holding a seat is not an error. The stream exists from registration, so work queues until a holder appears. Install the telegram module a week later and the backlog flushes.

6. The lifecycle: build, publish, deploy

Every shape above appears once, in order, and no step knows where the next one runs.

A change lands. The module holding mesh-git emits pushed — repository, ref, commit. An event, because it is a fact about git and git's identity is the meaning.

The change becomes work. The controller consumes pushed, asks the catalog which modules are built from that repository and path (ADR 0069), and submits one job per affected module to the mesh-build-machine seat. The builder stays simple: it builds what it is handed, and never resolves anything. A builder that dies mid-build has its job redelivered, because a work queue with explicit ack is what that means.

The artifact is published. The builder pushes to the registry seats and emits built — module, version, digest. An event again: a fact about the builder.

The build cascade is that event fanning out through a graph the mesh already has. A module whose image is built on another's artifact declares that in build.on. So built reaches the controller, which traverses the declared graph and submits rebuild jobs for everything downstream. A dependency cascade is not special machinery — it is one event, one derived graph, and the same job queue.

A declaration is state, not a message. The controller composes each affected node's declaration and publishes it last-per-subject. A node that was away gets exactly the current one, never a queue of superseded ones, and a replayed older one is refused by sequence.

A version prepares its state before it runs. Built 2026-09-28. A module version may declare an entrypoint that brings its state to the shape that version needs — the same vocabulary as the entrypoints it declares for its tools and its provisioner, and nothing about how a machine runs it. The mesh runs that entrypoint as it runs the module's own code, to completion, in the module's own context, and a version whose preparation did not succeed does not run: the step gates that module and nothing else on the machine (ADR 0136), and the rollout stops at the first machine that did not take it (ADR 0135, superseding ADR 0133). Once per state, and the mesh derives what a state is: a consumer is a module on a machine, so what the mesh provisions is per consumer and preparation is too. No level to choose, and no race to lock against.

Applying is reported to a role. The node-engine applies and reports to the mesh-controller seat — not to an address it was given at genesis. Held and retried while the store restarts (ADR 0083).

And the mesh says what it applied. Built 2026-09-28. A report is control traffic only the controller reads, so the chain above went dark at the moment it touched a machine: nothing said which version a machine now runs, or that it refused to. The controller states those as facts under its own seat's namespace, when what a machine runs changes rather than on every convergence pass, and anything that cares subscribes the way the catalog subscribes to built (ADR 0134). The facts are second-hand by design — one emitter, one ordering — and a machine that cannot reach the bus produces none, so absence is not health.

What disappears across that chain is every address. No webhook URL, no registered callback, no "which node is the builder on", no controller endpoint baked into a joining node. That is the class of bug issue 102 names, dissolved rather than fixed.

7. Modules depending on each other

Three kinds, and conflating them is how the order of sends goes wrong.

Build-time — A's image is built on B's artifact. Resolved by the cascade above; nothing at runtime cares.

Provision — A requires a database from B. A hard dependency: the credential must exist before A can start, so resolution gates delivery and A is shown as waiting until B has answered (design 27).

Seat — A uses B's seat. A soft dependency, and this is the one the bus changes. A starts whether or not anyone holds the seat, because the stream absorbs the gap. The order of sends stops mattering for everything expressed this way, and a service being restarted, moved or upgraded is not an outage for its callers — it is latency.

That difference is worth choosing on purpose. A dependency expressed as a provision must be ordered; the same dependency expressed as a seat need not be.

8. Versioning a protocol

A seat's protocol is a compatibility surface between modules that do not know each other and are deployed at different times. Four ways it can change, and they are not equally dangerous:

change example detectable
additive a new accepts subject, a new optional field nothing breaks
removal or rename send becomes deliver yes, mechanically
shape an optional field becomes required yes, if shapes are specified
semantic send starts meaning queue for tomorrow no

Additive is free. A seat may grow without a version, without re-registering a caller, and without ceremony. Most change is this.

A breaking change is refused while anyone is bound. Registration computes a compatibility fingerprint over the seat's protocol — its subjects and the shapes they carry. A registration that alters the fingerprint while callers are bound is refused, and the refusal names them. The mesh already holds the uses graph, so this is derived rather than declared, and it turns a runtime breakage into a registration-time conversation.

When a break is genuinely needed, the version goes in the subject, not the name. The seat stays one thing; mesh.seat.<seat>.v2.<verb> runs beside v1 and the holder serves both. A caller moves when it is ready. Versioning the seat name was considered and rejected: it forks the role, so "one holder" stops meaning one provider of the capability, and every document naming the seat has to be found and changed.

Binding is recorded, not inferred. A caller declares uses: telegram-sender with no version, and resolution binds it to the current one and records that — the same pin machinery design 27 already uses when resolution had a choice to make. Moving to v2 is a deliberate re-pin, so nothing drifts onto a new protocol because it happened to be newest.

Retirement is reported, never automatic. When the uses graph shows nothing bound to v1, the overview says it is retirable. The mesh does not remove it.

And none of this catches a semantic change. Same subject, same shape, new meaning: no fingerprint sees it, and no check proposed here would. The defences are review, and pushing meaning into shape wherever it can go — a required channel field is caught, a changed interpretation of an existing one is not. Saying so is better than implying the fingerprint is complete, because a team that believes it is complete stops reviewing for the case it misses.

A package, its series, and the series its users accept (from 2026-11-03)

(ADR 0606 §4, ADR 0609, from research 081.) This is the manifest's half of design 51's versions.

  • A package is declared as an artifact: its name, the kind package, its language (go or typescript), its paths and its package series (0.1). An image, a bundle or an archive may declare a package series the same way; one that declares none is numbered in 0.1. Nobody types a version: the builder counts it (design 51).
  • A package is named by its name in its own language (a design limit of the registry milestone, 2026-11-04): a Go package by the module path of its go.mod, a TypeScript package by the name in its package.json. The indexer reads that name at each merge, the builder numbers the package under it, the package's registry seat publishes it under it, and a user's packages names it so. One name per package across the mesh, because the language's own tools fetch by that name and no other. How it is checked: the manifest validator refuses a name in packages that no package of the shelf is named, or that a member of the change's delivery group does not name at its head.
  • Raising a package series is a reviewed change of the package's manifest, for a change that breaks its users. The checker's verdict names every change of a package series it sees, so the review of the change sees it.
  • A user names the mesh packages it takes in a section of its own, packages, each with the package series it accepts. It is not uses, which grants publishing on a seat's accepts subjects (§ 3); a package is not a seat. A user builds, and is checked, against the newest version of that series; a publish in another series does not move it, and moving is a reviewed change of the user's manifest. A user that names none accepts the series of the package's first numbered version, so nothing is edited in today's users. The indexer's tool indexer_package_users lists, per package, its users and the series each accepts, so a user left on an older series is seen.
  • Inside one repository a module uses its sibling package by path, from the same commit, and waits for no publish.
  • A user outside the package's delivery group is not built before the merge: it is built after it, when the new version is in its series. A user that breaks fails its own build, is not wanted, keeps its previous build, and a condition tells the operator. A group's members are built in the group's check, against the group's tagged version.
  • A contracts module's package names the directory of the seat definitions it is generated from in the field generated-from, the contracts marker (design 26).
  • The manifest validator reads the new words (ADR 0578 §1): each package series well formed and only on the four kinds that may carry it, a package naming go or typescript, and each name in packages a package some manifest declares. A running manifest validator that does not know a word sets the manifest aside to wait, never refuses it, while a newer indexer build is merged and not yet delivered; and a node-engine is handed a manifest using these words only once it states the capability to read them (ADR 0614 §4).
  • Cost: the new manifest fields are read through mesh-sdk's manifest types, so their change rebuilds every module once; it is the one rebuild the milestone of ADR 0612 already takes for the contracts move (ADR 0612 §1, ADR 0613 §1).

A config value renamed (from 2026-11-03)

(ADR 0609 §4.) A config value's declaration may name its old name, renamed-from. On the merge, mesh-config moves the value it holds under the old name to the new name in its own state, without the operator giving it again, emits renamed (module, old name, new name, when; its family config.events, kept seven days) and records it. This design decides what ADR 0609 leaves open:

  • Which values: a module's own config values only, never a secret, never another module's value. A rename keeps the value's trust: a value set without the operator's Approve stays untrusted under its new name (ADR 0348 §8), and the checker refuses a rename that would change a value's trust.
  • How long the old name is read (the rename window): until every node-engine whose node has a want of the module reports applied a build of it that reads the new name. mesh-config hears the status view's delivered events and, at its start and on each, asks its node_haves for every node, away ones included, since delivered counts only the nodes present (design 47); until then mesh-config answers both names. A node away when the last delivered was said should close its window when it returns, with no other delivery: mesh-config also wakes on the node-engine seat's event mesh.seat.node-engine.event.manifest-reads-changed, said when a node's stated capabilities change and when it returns after being away (ADR 0618 §4), as for the old seat name (design 51), and on it reads that node's stated capabilities through the seat's verb manifest-reads (ADR 0618 §3) and its applied builds through node_haves, never a watch of the node-engine's have state (ADR 0596). mesh-config declares seat:node-engine.manifest-reads in its invokes: that is the grant the grants part composes, and with it the grants part grants mesh-config the subscription to the event's subject (its unit row is in design 51). A node-engine that does not yet serve manifest-reads counts as stating nothing: the window stays open and the seat's verb renames names the node. ADR 0618 does not cover a returned node that applies the module's new build only after its return event, since that build changes what the module reads, not what the node-engine states: such a node closes the window on the next delivered, whichever delivery says it. A node holding a window open for seven days is mesh-config's finding naming the node, cleared when the window closes; config_rename_close drops a node from a window by hand, recorded as a hand act.
  • The checker asks the seat, never the module. The seat mesh-config gains the optional verb renames (the open renames, and the nodes holding each window open), in its seat definition (design 26). The checker asks it for every change carrying renamed-from or dropping one: a holder that does not serve it is refused by the tool runner naming the verb, and the checker then refuses renamed-from naming that; while a window is open, the same answer refuses dropping renamed-from, naming the nodes. A check that cannot ask fails, and says so. So no manifest uses renamed-from in the milestone that brings it, whose running mesh-config serves no renames; the first that does is its live case. The checker declares seat:mesh-config.renames in its invokes, so by the served-verb edge (ADR 0614 §2) its new build waits until a build of mesh-config that serves renames runs healthy; no cycle closes, since mesh-config waits only on the indexer's indexer_config_readers. The "not served" answer is then met only by the running checker in the milestone's own check, and after a revert. config_renames names, per rename, the nodes still holding the old name open.
  • A refused move (the old name holds nothing, or a secret) is mesh-config's finding, from which the condition keeper raises the condition, cleared once the value moved or renamed-from left the trunk's manifest. Tools of mesh-config: config_renames (what moved, from which name, when), config_rename_again (move one value by hand when a move was refused) and config_rename_close (above).

A value no longer read (from 2026-11-03)

A config value or secret no manifest on any trunk reads any more is a retiring value. mesh-config learns it from the indexer's tool indexer_config_readers, which it asks on each of the indexer's manifest-accepted events; the indexer says manifest-accepted for every newly accepted manifest, its bus-state entries empty when it asks none (the bus module reads an empty one as "this module asks nothing now", design 25 § 12.2). A tool the indexer does not serve yet answers "not known", and nothing retires on it. It is marked retiring only once no node-engine reports applied a build that reads it, as the rename window is (§ above). mesh-config then emits retiring (family config.events, kept seven days) and asks the operator once, through the operator channel, whether to remove it, naming any node that still read it until lately; it is removed only on the operator's Approve. mesh-config holds no secret, so the retiring flow and its one ask stay mesh-config's, and on the Approve it removes a value itself and retires a secret through the seat mesh-vault's new optional verb retire (one module's own secret, by node, module and name), carrying the operator's warrant, which the vault checks (ADR 0259 §6) before it removes anything. The vault's caller list for retire, a trusted config value in ADR 0363's form, names the holder of the seat mesh-config alone, checked against the caller the tool runner proved; the warrant is bound to the node, module and name it was asked for, so it retires nothing else. mesh-config then emits retired on config.events (module, name, node, the ask), and the vault emits own-retired (node, module, name, the ask's id, the caller it proved) beside its rotated and own-replaced, on the module events stream (ADR 0363) under the vault's own subjects, kept as that stream keeps every module's events. An ask unanswered for seven days is asked once more, and then written as a finding, cleared by an answer, a cancel, or the value read again; a sleeping node holding a retirement open for seven days is a finding naming it. config_retiring lists what is retiring, since when, and its ask, and asks it again or cancels it.

Where mesh-config keeps it. Its new state (renames, retiring, findings) and its stream config.events are declared in its old state and streams sections in this milestone, as the builder's and the checker's new state is (design 51 The move), so that neither mesh-config nor the deciding parts that ask it wait on the bus module's process, which is delivered in the same milestone. They move to bus state when each module next moves (ADR 0595 §11). config-changed stays on the shared events stream, unchanged, so its consumers do not move.

How the rename and the retiring are checked, as cases of mesh-config's first whole-module integration test (C20, C24 below), of the checker's and of the vault's existing whole-module tests: the checker's ask of renames meeting a holder that does not serve it refuses renamed-from naming the verb, one meeting an open window refuses dropping renamed-from naming the nodes, and a check that cannot ask fails saying so; retire with the warrant removes the secret and emits retired and own-retired, without it, with a warrant for another name, or from a caller not on the list it is refused; a waiter on the vault's rotated or own-replaced does not wake on own-retired; a node that was away, returning with a build that reads the new name, closes the rename window on its manifest-reads-changed event when it applied that build before the event (ADR 0618 §4), and on the next delivered when it applies it after; config_rename_close drops a node from a window and records the hand act; a window held seven days and a retirement held seven days each write their finding naming the node, cleared when it closes.

The mesh delivery cases this design carries

Research 081 listed the mesh delivery cases; these are the ones it handed to a design, each with its acceptance test, each a case of its module's one whole-module integration test unless it says live.

case what holds acceptance test
C6, a package series raised users on 0.1 keep the newest 0.1; a reviewed change moves a user builder: a 0.2.0 publish moves no 0.1 user, its check takes the newest 0.1 too, a reviewed change naming 0.2 moves it, a user naming no series builds against the newest 0.1
C14, a provision's form gains a field the provider serves it in a new version; the consumer reads it once rebuilt; an older consumer ignores it mesh-sdk: a consumer built against the older form reads a provision with the new field
C18, a new required config value with no default the check names the nodes that lack it; the want waits; the operator is asked once live: a module gaining such a value waits on each node lacking it, the operator is asked once naming the nodes, and once given the module is applied
C20, a config value renamed the value moves, keeps its trust, both names read until every node runs the new build mesh-config: a renamed value is present under the new name with no operator input and its trust; the old name answers until the node-engines report the new build; dropping renamed-from early, or changing trust, is refused; a refused move writes its finding
C22, a secret only a person can give the want waits; the operator is asked once what to give and where live: a module requiring such a secret waits, one ask names it and where, and once given the module starts
C24, a config value or secret no longer read it is retiring, the operator is asked, removed only on the Approve mesh-config: with one node-engine still reporting a build that reads the value it is not marked retiring and no ask is sent; once that node reports the new build it is marked retiring, retiring is emitted and one ask reaches the operator; an ask unanswered seven days is asked once more, then written as a finding; the value stays until an Approve removes it

9. Provisioning over the bus

Provisioning rides the bus, and the provider stops having an address.

part of a provision shape
the requirement resolving to a provider the controller's, not the bus's
the grant reaching the provisioner request/reply to a role
holds — the reconcile question, every minute the same call, on a timer
provisioned / deprovisioned events
the credential reaching the consumer §10 — fetched, never carried

A provisioner's interface is already three calls — create, remove, holds — which is exactly a serves protocol. So a provision interface is a seat whose protocol is those three, which is why design 26 already allows a seat to deliver a provision. The two concepts were converging before this document; here they meet.

What stays different, and must not be unified away: a provision has a per-consumer resource and a sealed credential, created and destroyed per consumer. A seat protocol has neither — it is a role you send to. Collapsing them would mean pretending a database is a subject.

mesh-bus and nats are two interfaces, never one name

The mesh's own bus is mesh-bus, delivered by the mesh-broker seat and answered by the controller — because the bus's accounts are configuration rather than something a provisioner creates, so there is no provisioner process in the path and nothing waiting on a bus account in order to make bus accounts. A module that runs a NATS server of its own and offers it as a backing service provides nats, exactly as the deprecated broker provides amqp (ADR 0127 (superseded by ADR 0131: AMQP is not a provision, and everything speaks to the mesh-broker seat)).

They are never the same name. A manifest saying nats could otherwise mean either the mesh's nervous system or a private queue, and the difference between those is the whole architecture. 0119's rule decides which is legitimate: a private bus is a backing service, never a channel to another module.

Where addresses survive

"Where is it?" is two different problems, and the bus solves one of them completely and the other not at all. Keeping them apart matters, because a reader who thinks the mesh no longer has addresses will believe a class of bug is fixed when it is untouched.

Something that is on the bus: the address disappears. A host reporting in used to need the controller's address — recorded somewhere, at some moment, and wrong as soon as anything moved. Now it publishes to mesh.seat.mesh-controller.report and the bus routes it to whoever holds the seat. Nothing anywhere records where the controller is, so nothing can record it wrongly. The same is true of the builder, the catalog, the telegram sender. This class is not mitigated; it is gone, because the information is no longer stored.

Something that is not on the bus: the address stays, exactly as before. A module that requires a database does not reach postgres over NATS — it opens a postgres connection, because postgres speaks postgres and is not listening on any subject. Its credential contains a host and a port, and no amount of subject addressing changes that.

So the fix for that second class is unchanged and is not this document's: ADR 0098 — fetch the fact where it is used rather than storing a copy — which is what issue 102 is actually about. The bus makes that class smaller by removing every mesh-internal address from it. It does not make it empty.

And one address is irreducible: the bus's own. A node has to know where the broker is before it can use subjects for anything, so that one cannot be a subject. It is the addressing equivalent of §10's bootstrap — the first thing cannot be found by the mechanism that finds everything else.

10. Secrets, and why they never enter a stream

Everything the vault does is request/reply to the mesh-vault role: mint, fetch, rotate. In that sense it is as much on the bus as anything else.

The bus is not trusted with a secret, and does not need to be. A secret is sealed to its recipient, so what crosses the bus is ciphertext only that recipient can open. The broker sees that a secret moved, and to whom — metadata, which is acceptable — and never a plaintext.

But sealed is not enough on its own, because a stream persists. A sealed secret written into a JetStream stream is a durable ciphertext sitting in the mesh's own storage, and the day a sealing key leaks, that stream is an archive rather than a moment. So:

  • A secret travels on core request/reply, never through a stream. No persistence, no replay, nothing to exfiltrate later.
  • A declaration names a secret; it does not carry one. Declarations are the state shape, which is a stream — so the node-engine fetches the secret from the vault at apply time, over the core path. That is ADR 0098's existing discipline — fetched from it, not carried — applied to the one payload where carrying it is worst.

A key-value bucket is a stream, so the same holds for it. Added 2026-10-04, ADR 0201. No secret is put in a module's state, sealed or not: state is exactly what a machine joining a year later reads in full. A value that needs a secret names it, and the secret travels on request/reply. Sealed values are plain text to anything inspecting them, so this is checked only partly — the runtime refuses a value carrying a field whose name says it is a credential, which catches the ordinary mistake and not a determined one.

The bootstrap, which is circular and has a precedent. The vault makes every secret (ADR 0113), including the bus's own passwords. The vault is a module, and a module needs a bus account, whose password the vault makes. Nothing can go first.

This is the shape ADR 0067 already resolves for the controller: genesis is a pivot. The controller mints the handful of foundation credentials itself, raises the store, the broker and the vault, and then the vault takes over and mints everything from there — the same move as raising a temporary controller and reinstalling it as an ordinary module once the registry exists.

So there are exactly two things the normal path cannot make, both at genesis, both ending the moment the mesh can mint for itself: the bus's own accounts (§the bootstrap argument in ADR 0127 (superseded by ADR 0131: AMQP is not a provision, and everything speaks to the mesh-broker seat) (superseding ADR 0125) — a provisioner is a module and needs an account before it can run) and the vault's own credential. Any third exception is a design failure, and naming these two is what makes a third one visible.

What a step is answerable for, 2026-10-02

ADR 0187. A step gates its module and not the machine (ADR 0136), but a step that exits non-zero still leaves the machine reporting that it is not doing what it was told — and a clean apply gates other things entirely, the found firewall's retirement among them. So what a step calls a failure matters beyond the step.

The rule the media step now follows, and the one to copy: a step fails for what the mesh chose and can fix, and reports what it merely found and cannot. An indexer entry the mesh re-pointed at this mesh's proxy is plumbing the mesh is answerable for; whether the public tracker behind it answers today is not. An entry the operator listed is the operator's to insist on, and still fails. Six hours of a machine reading as broken, for one tracker that had died, is what the distinction costs when it is missing.

11. Open

Semantic change has no mechanical defence (§8). Recorded as open rather than solved, because it is the residue of a question the rest of §8 answers and the part a fingerprint cannot reach.

Whether a module may declare a seat it does not itself claim — the contract as one thing, the implementation as another, which is how two competing implementations would ever exist.

Whether a container should have a readiness notion. Only an action carries verify, so a step that must run once a service answers — seeding through its own API — cannot be declared at all. Named here because the steps above make the gap obvious, not because they caused it.

Whether consumes naming another module couples too tightly. It is kept here deliberately — an event's provenance is its meaning — but a consumer of billing.order.placed does depend on billing existing under that name.

12. How it is checked

  • A manifest holds no subject. A catalog test: no manifest contains a string matching the subject grammar. The rule is worthless if it is followed by convention.
  • A preparation is given what the module is given. A composition test: what the preparation entrypoint receives equals what the module's own code receives, asserted rather than written twice — which is the drift a hand-written step invites, three times over in the catalog today.
  • A convergence that changed nothing says nothing. Two identical reports, one emitted fact: what is guarded against is a fact per minute per machine, which is a stream nobody reads.
  • Permissions are exactly the three namespaces. A composition test per module: the derived permission set equals what its declaration implies, and a hand-written addition to it fails.
  • A sender cannot read the queue it writes to. A bed: a module declaring uses is refused subscribe on that seat's inbound subject.
  • One holder, one delivery. A bed: a seat's job delivered once with the holder running, and a second claim of the seat refused.
  • A queued job survives no holder. A bed: submit with the seat unheld, assign the holder, the job is delivered.
  • The cascade rebuilds exactly the dependents. A bed: publish an artifact two modules build on, and exactly those two are rebuilt.
  • A stale declaration is refused. A bed: replay sequence n−1 after n, and the node refuses it rather than applying it.
  • A module reaches only the state it declared. The composer's test: an owner's runtime may write its buckets, a reader's may only read, and nothing else is granted; the runtime's test over a real bus: a name not issued and a reader's write are refused with the reason.
  • Bus state is asked for once, in one form, and read by its owner alone (ADR 0595 §8, ADR 0596). The manifest validator's unit tests: a reads in the new form, a local name used twice across storage types or in an old section and the new form, an unknown template and an option outside its bound are each refused, naming the field. Live: a moved module receives its binding after created; another module's account reading its entries is refused; a moved reader hears its owner's declared event and asks its owner's tool. The rest is design 25 § 12.7.
  • State is current at once. The runtime's test over a real bus: a watch hands the current values without deletions, then an end-of-current marker, then changes. Live: a machine assigned after a put reads it at start.

Reviewed by architectural domain

The amendment of 2026-11-03 to ADR 0595, ADR 0596 and ADR 0597 (designs 25 § 12, 32 § 4 and 55 together) was reviewed by the four reviewers by architectural domain side by side (playbook 01), over three rounds, and the lead ruled on every finding. What was found, and what answers it:

Domain Found Answered by
security the author could be words the module chose; a person's dangerous tools had no role, mark or path to the process; the caller travelled as a plain field design 25 § 12.3 (marks, the process's own tool subject, the identity seat's signed token), § 12.4 (the tool runner stamps the caller it proved; the move's authors marked imported)
seams the bus module asked for its own events in the new form, which it would have to create itself (ADR 0595 §10); a person's tool had no join to the process; a verdict was written twice with no order design 25 § 12.3 (its events on its old streams section; the process's own tool subject); design 55 § 2.1 (the review operation first, reviews caught up from it)
running qualities overdue creation was undefined and raised by nobody; the database's rollback write lived in memory; a forward delivery after a rollback could miss acceptances and verdicts; no size estimate of the heads design 25 § 12.2 (five minutes, the watchman, durable consumers, check at start); design 55 § 8.1 step 6 (database.sequence, the check at every start before the drop), § 2.1 (the heads' estimate)
health the bus module's health would have fired on a quiet mesh; conditions had no clearing; database-behind had no clearing design 25 § 12.2 and § 12.3 (each condition with what clears it); design 55 § 8.1 step 6
events and logs an event lost when the module stopped after apply; the bus module's events had no reader design 55 § 1 (each event sent with its operation's sequence as id, the state sent); design 25 § 12.3 (history of one entry)
testing the bus module had no test that starts it whole; live tests would have filled the live bus; the budget was not counted design 25 § 12.7 (the nats module's integration test, live drift only by a manifest change, the budget paragraph); design 55 § 10
upgrade the build order lacked the grants part's binding and two undo steps design 25 § 12.6
tools the ledger's repairs had no tool for the next owner; the bus module's events had no read tool design 25 § 12.5 (served for every ledger owner), § 12.3

Left to the operator, because each changes an accepted record: a margin between raising and clearing the condition full (ADR 0595 §6 raises and clears it at 75 percent, so an entry sitting there flaps), and an event of its own for drop and recreate. A correction of fact for ADR 0595 §4: the bus module deletes through recreate as well as drop. Each is noted on issue 1966.

The amendment of 2026-11-03 for research 081 (ADR 0606, ADR 0609; the package, rename, retiring and delivery-case sections of § 8) was reviewed with designs 51 and 26 by the four reviewers by architectural domain; the findings and what answers them are listed in design 51's Reviewed by architectural domain. For this design: a rename keeps a value's trust (security), packages is a section of its own, not uses, and mesh-config hears the status view's delivered events (seams), a value retires only once no node-engine runs a build reading it (security), and renamed and retiring have a family and an ask that is asked again (events).

The milestone's amendment (ADR 0612, ADR 0613: the seat verb renames the checker asks, the vault's retire, manifest-accepted for every manifest, mesh-config's new state in its old sections) awaits its own review by architectural domain, which rewrites this section.