The mesh's own seats said who does a job and nothing about what may be said to them or by them, and that gap showed up three times in one day looking like three different problems: a build machine with three audiences for one outcome and no way to derive a grant for any of them; an event genuinely about a role with nowhere to live but the namespace of whichever module holds that role today; and a catalogue catching up on builds, where every option needed a grant the design refuses. One cause — the mesh has roles it cannot describe. So the `mesh-*` seats take the same three fields a module's seat has, and the machinery that already derives authority, queues and consumers from a declared seat does it for these too. Builds become work submitted to a role, and `mesh.build.request`, `mesh.control.built` and the BUILDS stream retire. A work queue shared by several build machines is exactly what a seat's `accepts` is, so a second mechanism for it was two places a permission could be wrong. The outcome is the seat's own event, which means one publish still reaches whoever asked, the controller that records it and the catalogue that places it — the fan-out a shared exchange gave for free, written as a subject the mesh derived rather than a topology somebody configured. That also avoids the grant that ruled out the alternatives: no holder needs permission to publish into an asker's inbox. The blocking gap is now named rather than incidental: the shared library has no way for a module to publish on a seat. The build machine is Go and reaches the bus directly, so it is unaffected; the artifact-store event waits.
462 lines
28 KiB
Markdown
462 lines
28 KiB
Markdown
---
|
||
layer: to-be
|
||
status: proposed
|
||
code: []
|
||
updated: 2026-09-27
|
||
decisions:
|
||
- 02-DECISIONS/0118-a-module-declares-its-own-seats.md
|
||
- 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md
|
||
- 02-DECISIONS/0120-the-mesh-bus-is-required-not-ambient.md
|
||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||
- 02-DECISIONS/0041-events-are-a-relationship.md
|
||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
|
||
- 02-DECISIONS/0121-a-seat-carries-the-protocol-of-its-role.md
|
||
---
|
||
|
||
# 29. 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 0120](../../02-DECISIONS/0120-the-mesh-bus-is-required-not-ambient.md)). 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](25-the-bus-on-nats.md) is the bus itself —
|
||
subjects, streams, accounts, enrolment — and stays the authority on the wire.
|
||
[Design 19](19-the-module-protocol.md) is the specification an SDK implements, and is rewritten
|
||
onto this in step 3 of [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md).
|
||
|
||
## 1. A module names locally; the mesh derives the subject
|
||
|
||
This is the load-bearing rule.
|
||
[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) says a module
|
||
definition 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 catalogue, 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 |
|
||
|
||
**Wildcards, and they are the mesh's rather than a bus's.** *Added 2026-09-27, from
|
||
[issue 127](../../04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md).* 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 catalogue, 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](25-the-bus-on-nats.md) §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 catalogue is still correct. That is the property
|
||
[ADR 0039](../../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md) 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.
|
||
|
||
## 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](25-the-bus-on-nats.md) §3).
|
||
|
||
**The mesh's own seats carry protocol too.** *Added 2026-09-27,
|
||
[ADR 0121](../../02-DECISIONS/0121-a-seat-carries-the-protocol-of-its-role.md).* 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 build machine 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 machine 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 catalogue 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 | `serves` |
|
||
|
||
**Job** is the one [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) 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](../../04-ISSUES/107-a-declaration-carries-no-order/00-report.md).
|
||
|
||
## 5. Seats
|
||
|
||
A module declares a seat with its protocol, and the mesh enforces one holder at its scope
|
||
([ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md)). 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 catalogue which modules
|
||
are built from that repository and path
|
||
([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)), 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 walks 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.
|
||
|
||
**Deployment 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.
|
||
|
||
**Applying is reported to a role.** The host 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](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)).
|
||
|
||
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](../../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
|
||
names, dissolved rather than fixed.
|
||
|
||
## 7. Modules depending on each other
|
||
|
||
Three kinds, and conflating them is how deployment ordering 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](27-a-module-requires-the-mesh-resolves.md)).
|
||
|
||
**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. Deployment order 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](27-a-module-requires-the-mesh-resolves.md) 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.
|
||
|
||
## 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](26-the-seats.md) 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 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md)).
|
||
|
||
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 catalogue, 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](../../02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md) —
|
||
fetch the fact where it is used rather than storing a copy — which is what
|
||
[issue 102](../../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
|
||
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 host fetches the secret from the vault at apply time, over the core path.
|
||
That is [ADR 0098](../../02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md)'s
|
||
existing discipline — *fetched from it, not carried* — applied to the one payload where carrying
|
||
it is worst.
|
||
|
||
**The bootstrap, which is circular and has a precedent.** The vault makes every secret
|
||
([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)), 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](../../02-DECISIONS/0067-genesis-is-a-pivot.md) already resolves for
|
||
the control plane: **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 control plane 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 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)) — 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.
|
||
|
||
## 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 `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 catalogue test: no manifest contains a string matching the
|
||
subject grammar. The rule is worthless if it is followed by convention.
|
||
- **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.
|