Merge main: the bus design, issues 113/114/117/120 and research 017 landed
# Conflicts: # 04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
status: resolved
|
||||
opened: 2026-09-23
|
||||
located-in: [mesh-host internal/apply]
|
||||
fixed-by: mesh-host PR #22 — a container records the digest of every file it reads at creation, its env-files and files mounted into it directly, and is recreated when one changes; a mounted directory still needs restart-on
|
||||
fixed-by: mesh-host PR #22 — a container records the digest of every file it reads at creation, its env-files and files mounted into it directly, and is recreated when one changes; a pre-upgrade label is accepted once, and the plan names the file. A mounted directory still needs restart-on.
|
||||
amended-design:
|
||||
---
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-09-24
|
||||
located-in: [mesh-catalog modules/minio]
|
||||
fixed-by:
|
||||
fixed-by: mesh-catalog — the object-store module repinned to a maintained fork of the withdrawn server image, its runtime sidecar built from source rather than pulled, and its data moved off the predecessor's live directory. The standing condition this report names is not closed by it — see What was done.
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -109,6 +109,28 @@ a registry the mesh does not control — **by tag or by digest, it makes no diff
|
||||
dependency with no guarantee behind it, and the mesh currently learns it has lost one only by
|
||||
trying to use it.
|
||||
|
||||
[Issue 064](../064-a-mesh-build-cannot-fetch-a-modules-external-dependencies/00-report.md) is the
|
||||
nearest precedent, and it does not cover this. That issue asked whether the mesh's build
|
||||
environment can **reach** a declared vendor image — a network-policy question, answered by
|
||||
requiring the image be declared as a build input — and it assumed that an image, once declared,
|
||||
stays fetchable. Withdrawal is the case the assumption does not cover: no network policy and no
|
||||
declaration makes a deleted repository resolvable, so a module can satisfy 064 in full and still
|
||||
be unbuildable on a node that holds nothing.
|
||||
|
||||
## What was done
|
||||
|
||||
The module was repinned to a maintained fork of the server image, published to a registry that
|
||||
still serves it; its runtime sidecar is now built from source rather than pulled; and its data was
|
||||
moved off the predecessor's live directory. The object store runs on the control-node from that
|
||||
pin, and a node holding nothing can obtain it again.
|
||||
|
||||
That answers the instance and none of the three points above. The mesh still cannot say which of
|
||||
its other pinned third-party images are still obtainable, and it would still learn of a withdrawal
|
||||
only when a node without the image tried to deploy. The replacement question — S3 the protocol
|
||||
rather than this product — is carried by
|
||||
[research 015](../../01-RESEARCH/015-the-object-store-after-minio/00-overview.md); the detection
|
||||
question is carried by nothing, and is the first of the open questions below.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Should the mesh **hold** the images it depends on — mirroring third-party images into its own
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-24
|
||||
located-in: [mesh-controller module.json, mesh-host internal/apply]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 114 — Should the controller run as a container, or as a process the host supervises directly?
|
||||
|
||||
## What was observed
|
||||
|
||||
On the control-node, 2026-09-24, over a long session of operating the mesh through
|
||||
`mesh-controller`'s CLI (build, push, plan, status, module moved). Every mutating step reached the
|
||||
binary the same way: `docker exec mesh-controller /mesh-controller <command>` — because
|
||||
`mesh-controller`'s own manifest declares its one resource as:
|
||||
|
||||
```json
|
||||
{ "id": "server", "type": "container", "name": "mesh-controller", "network": "host", "args": ["serve"] }
|
||||
```
|
||||
|
||||
Two things about that declaration are worth naming together, because neither is a problem on its
|
||||
own and the combination is what raises the question:
|
||||
|
||||
- **`network: host`.** The controller does not use container network isolation, which is the
|
||||
property a `container` resource type usually buys over a `process` one. It runs with the node's
|
||||
own network namespace either way.
|
||||
- **It is the mesh's single point of coordination.** [`03-DESIGN/01-to-be/06-the-controller.md`](../../03-DESIGN/01-to-be/06-the-controller.md)
|
||||
is explicit: "one node runs it, and nothing takes over" — no election, no quorum, no failover;
|
||||
recovery is restore, not failover.
|
||||
|
||||
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) gives the host — the one thing tier 0 requires
|
||||
to be a real system daemon — exactly this reasoning for refusing to run in a container: *"installing
|
||||
the container runtime is a step of the bootstrap, so a host inside a container would need the thing
|
||||
it exists to install."* The controller is one tier up and does not install the runtime, but it
|
||||
shares the profile that argument turns on: something the rest of the mesh's operation depends on,
|
||||
sharing fate with a runtime that is not itself.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
Practically, tonight: every controller interaction was raw shell into a container (`docker exec`),
|
||||
not a first-class surface — no logs command beyond `docker logs`, no `systemctl status`, and a
|
||||
session permission classifier that (correctly) treats arbitrary shell into a container as needing
|
||||
sign-off every time, unlike an ordinary supervised process. That friction is a symptom, not the
|
||||
issue itself.
|
||||
|
||||
The actual question is whether `type: container` is buying the controller anything here besides
|
||||
image-based delivery and a restart policy — both of which [ADR 0005](../../02-DECISIONS/0005-the-node-host.md)'s
|
||||
launcher pattern already describes as buildable directly into the host's own supervision (restart on
|
||||
exit, count consecutive failures, roll back after too many, halt after that), for the host's own
|
||||
unit. If the controller were declared `type: process` instead — still built and versioned through
|
||||
the same delivery pipeline, just executed on the node and supervised by the host the way the host
|
||||
supervises itself — it would stop sharing fate with the container runtime's health (restarts,
|
||||
upgrades, disk pressure evicting containers) for the one piece of software whose absence the rest of
|
||||
the mesh is designed to tolerate but nothing is designed to *want*.
|
||||
|
||||
This is squarely a question, not a claim that today's shape is wrong: [ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)
|
||||
already tolerates the controller being down by construction (nodes reconcile from their own
|
||||
last-applied state), which may make the container-runtime coupling moot in practice. Nobody has
|
||||
checked.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Does `mesh-host`'s `process` resource type already support the restart/failure-counting semantics
|
||||
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) describes for the host's own launcher well
|
||||
enough for something this central — or would this need host-side work first?
|
||||
- With `network: host` already in use, what does `type: container` provide the controller today that
|
||||
`type: process` would not?
|
||||
- Is there a real circularity risk — the controller's own health depending on the container runtime
|
||||
it (indirectly, via the host) manages — or does "one node runs it, nothing takes over" already make
|
||||
a controller outage tolerable regardless of which resource type it is?
|
||||
- If the answer is "keep it a container," what does that answer, precisely, that this issue asked —
|
||||
so the next person who notices the same asymmetry finds it answered rather than open again?
|
||||
|
||||
## The general case
|
||||
|
||||
[Issue 117](../117-a-modules-own-code-is-a-container-and-a-process/00-report.md) is the same
|
||||
question asked of every module rather than of the controller: a module's own code is a `container`
|
||||
in [ADR 0047](../../02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md)
|
||||
and a `process` in the to-be design, and no record moves it. Its
|
||||
[diagnosis](../117-a-modules-own-code-is-a-container-and-a-process/01-diagnosis.md) answers the
|
||||
first open question above: the host's `process` shape is built, applied and tested, including the
|
||||
restart and run-to-completion semantics — so this would not need host-side work first.
|
||||
|
||||
The two do not collapse into one. The controller is not a code-carrying sidecar, and `network: host`
|
||||
is what makes the asymmetry visible here and nowhere else.
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-09-25
|
||||
located-in: [hq, mesh-catalog modules/showcase, mesh-sdk src/tools/index.ts, mesh-tools]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 117 — A module's own code is a container in one record and a process in another
|
||||
|
||||
## What was observed
|
||||
|
||||
Asked what the "sidecar" is — the second container a code-carrying module runs beside its
|
||||
service — and whether a supervised process would do instead. Reading the records to answer it,
|
||||
the repository answers both ways, and nothing reconciles them.
|
||||
|
||||
| record | status | what runs a module's own code |
|
||||
|---|---|---|
|
||||
| [ADR 0047](../../02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) | **accepted**, 2026-09-04 | "a **container**, the tool runtime carrying that module's compiled code" — one module, one process, one account; events and tools in that same process, "not a second one to scope and seal" |
|
||||
| [`01-to-be/18-building-a-module.md`](../../03-DESIGN/01-to-be/18-building-a-module.md) | proposed, 2026-09-21 | a resource type table in which `container` is "an image" and **`process`** is "**its own code**, in three modes", whose default mode is "a unit restarted when it exits", supervised by the machine |
|
||||
| [`01-to-be/20-writing-a-module.md`](../../03-DESIGN/01-to-be/20-writing-a-module.md) | proposed, 2026-09-21 | one module declaring **four** `process` resources — events, tools, provisioner, a scheduled ingest — each with its own `run` argv, and the sentence "it is why these are `process` rather than four containers" |
|
||||
|
||||
Three disagreements, not one:
|
||||
|
||||
1. **Container or unit.** ADR 0047 chose a container and said why: a node-wide runtime loading
|
||||
every module's code could not hold a per-module account, so the runtime is per-module. The
|
||||
design docs choose a supervised unit running an argv and give no reason, because they do not
|
||||
record that they are choosing.
|
||||
2. **One process or several.** ADR 0047's "one module, one process, one account" is the whole
|
||||
content of its second and third sections. The worked guide declares four for one module and
|
||||
presents four as the point.
|
||||
3. **Whether the record was consulted at all.** Neither design doc names ADR 0047 in
|
||||
`decisions:`. No record supersedes or extends it on this. **The string `process` as a resource
|
||||
type appears in no decision record** — the shape exists only in two `proposed` design docs.
|
||||
|
||||
Meanwhile the thing as built is the container. [ADR 0029](../../02-DECISIONS/0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md)
|
||||
records that "anything that is a service plus a sidecar currently has to publish a port to talk
|
||||
to itself," which is one of the things the host's `network` shape was added for.
|
||||
[Issue 113's diagnosis](../113-the-object-stores-images-were-withdrawn-upstream/01-diagnosis.md)
|
||||
found a catalogue module declaring "two container resources," the second a runtime sidecar
|
||||
"pinned at an all-zeros digest, meaning nothing was ever published for it."
|
||||
[Issue 095](../095-a-module-assigned-after-genesis-has-no-broker-account/00-report.md) is a
|
||||
sidecar crash-looping on a credential while its service served correctly.
|
||||
[ADR 0093](../../02-DECISIONS/0093-a-fixture-that-runs-a-modules-runtime-carries-its-name.md)
|
||||
records that a bed wanting "a sidecar without its server raises the server."
|
||||
|
||||
### And the word is in no glossary
|
||||
|
||||
"Sidecar" appears sixteen times across five records — two decisions and three issues. It is
|
||||
absent from [`00-META/glossary.md`](../../00-META/glossary.md), and absent from every document
|
||||
under [`03-DESIGN/`](../../03-DESIGN/), in both layers. ADR 0047, which creates the thing, never
|
||||
uses the word; it says "runtime process" and "runtime container". The glossary's own rule is that
|
||||
"a new name for an existing thing lands here first, in the same change that introduces it in
|
||||
code," and the page exists because "the terms kept drifting in conversation." A reader asking
|
||||
what the sidecar is has nowhere in the design layer to look, which is how this was found.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
- **A module author reading the current guide writes a `process`; the catalogue as built declares
|
||||
a `container`.** [`20-writing-a-module.md`](../../03-DESIGN/01-to-be/20-writing-a-module.md) is
|
||||
a worked guide with a manifest in it. Whichever of the two is wrong, somebody follows it.
|
||||
- **The cost of the container shape is paid in four places and totalled in none.** A published
|
||||
image per code-carrying module, a network so a module can reach itself, a bed that cannot run a
|
||||
runtime without raising the server it manages, and a credential failure that presents as the
|
||||
module's own bug. Each record argues its own piece is worth paying. No record puts them beside
|
||||
the alternative.
|
||||
- **Both shapes carry a cost the other does not, and neither is written down.** A container
|
||||
carries its own interpreter; a `process` declaring `run: ["node", "index.js"]` needs an
|
||||
interpreter present on the machine, which is the machine dependency the statically linked host
|
||||
([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)) exists to avoid. And `run` is an argv,
|
||||
where [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) refuses `action` because the link may
|
||||
not carry a command — a refusal [`18-building-a-module.md`](../../03-DESIGN/01-to-be/18-building-a-module.md)
|
||||
restates on the same page that it introduces `process`.
|
||||
- **This is the repository's own named failure mode, in its own records.** `cycle.py` enforces
|
||||
that a to-be doc names *at least one* decision. Both docs do, so both pass, while introducing a
|
||||
resource type no decision records and contradicting an accepted one. The rule is "no design
|
||||
without a decision"; the check is "no design without *a* decision." An unenforced rule is
|
||||
indistinguishable from a wrong one, and these two documents are what that gap looks like when
|
||||
something walks through it.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Which is the decision — container or supervised unit? If the design docs are right, ADR 0047
|
||||
needs superseding rather than quietly outliving. If ADR 0047 is right, two proposed documents
|
||||
and a worked manifest describe a resource type that does not exist.
|
||||
- Is one account per module satisfied by a per-module *unit* as well as a per-module *container*?
|
||||
ADR 0047's argument rules out a node-wide runtime sharing one account. It does not appear to
|
||||
rule out a unit holding one scoped credential, and nothing has said so either way.
|
||||
- If several processes for one module are right, what holds the accounts? ADR 0047 refused "a
|
||||
second one to scope and seal" for events beside tools. Four processes are four somethings.
|
||||
- How does a `process` get its interpreter, and does declaring one reintroduce the machine
|
||||
dependency the host is built to avoid?
|
||||
- Is `run` an argv the link may carry, given `action` is refused for being one? If the answer is
|
||||
that a `process` reconciles and an `action` does not, that distinction is not written down.
|
||||
- What is the thing called, and where does the design layer describe it? Whichever shape wins, no
|
||||
document in either layer currently says a code-carrying module runs a second thing beside its
|
||||
service.
|
||||
- **How would this have been caught?** A decision and a design doc disagreeing on a resource type
|
||||
is mechanically checkable: the resource types a design doc names are a closed set, and every
|
||||
member of it either appears in a decision or does not. Whether that check is worth writing is
|
||||
part of this issue, not settled by it.
|
||||
@@ -0,0 +1,201 @@
|
||||
# Diagnosis — 117
|
||||
|
||||
## Which trees were searched, 2026-09-25
|
||||
|
||||
Named first, because [issue 113](../113-the-object-stores-images-were-withdrawn-upstream/01-diagnosis.md)
|
||||
is the record of reporting absence in one repository as absence in the mesh.
|
||||
|
||||
| Searched | At |
|
||||
|---|---|
|
||||
| `mesh-host`, `mesh-catalog`, `mesh-tools`, `mesh-sdk`, `mesh-controller` | `main`, fresh shallow clones |
|
||||
| `hq` | `main`, and the two branches named under finding 7 |
|
||||
|
||||
**Not searched:** the private migration repository; the open pull requests on the catalogue and
|
||||
the controller; any branch of a code repository other than `main`. A statement below about "the
|
||||
catalogue" is a statement about its `main`.
|
||||
|
||||
## The report's central question is answered: the shape exists
|
||||
|
||||
`mesh-host` `internal/declaration/declaration.go` defines `TypeProcess Type = "process"`.
|
||||
`internal/apply/process.go` applies it — it writes the unit, writes the timer for a scheduled one,
|
||||
and gates what follows a run-once one. It has tests of its own in both packages. The resource
|
||||
carries a bundle `source` with a `digest`, a `run` argv, `env` and `env-file`, a `user`,
|
||||
`restart-on`, and the `run-once` and `schedule` modifiers.
|
||||
|
||||
So the report's alternative — "if ADR 0047 is right, two proposed documents and a worked manifest
|
||||
describe a resource type that does not exist" — is **disproven**. It exists, it is implemented, it
|
||||
is tested, and the host's vocabulary is now **twelve** shapes rather than the nine
|
||||
[ADR 0029](../../02-DECISIONS/0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md)
|
||||
counted.
|
||||
|
||||
## The enforcement ADR 0029 asked for is intact, and it recorded this gap rather than closing it
|
||||
|
||||
ADR 0029 said "the vocabulary is nine, and the count moves with a record. The test that asserts it
|
||||
names this one." That test exists — `internal/declaration/declaration_test.go` asserts the count is
|
||||
twelve and fails with the reason rather than a number. Above the assertion, a paragraph per
|
||||
addition names what made it one:
|
||||
|
||||
| shape | the test names |
|
||||
|---|---|
|
||||
| `network`, ninth | ADR 0029 |
|
||||
| `access`, tenth | ADR 0051 |
|
||||
| **the eleventh** | **`03-DESIGN/01-to-be/18-building-a-module.md`** — a design document, `status: proposed` |
|
||||
| `opening`, twelfth | ADR 0100 |
|
||||
|
||||
The eleventh is this one. The test still calls it `daemon`, the code calls it `TypeProcess`, and
|
||||
its paragraph is the only one that names a design document where the others name a decision.
|
||||
Independently: in `declaration.go`, `TypeProcess` is the **only** shape in the vocabulary whose doc
|
||||
comment cites no ADR — `network` cites 0029, `access` 0051, `opening` 0100, `user` and the refusal
|
||||
of `action` cite 0005.
|
||||
|
||||
**So ADR 0029's mechanism worked exactly as designed and was not enough.** It requires every
|
||||
addition to name something. It does not require that something to be a decision, and the one
|
||||
addition that named a proposed design document instead is the one this issue is about.
|
||||
|
||||
### A correction to this trail, recorded because it was one grep from being a finding
|
||||
|
||||
The first search here was for `len(Vocabulary())` and found nothing, and the working conclusion for
|
||||
two steps was that no count assertion existed any more — which would have been written up as "the
|
||||
mechanism ADR 0029 relied on is gone." It is not gone. The test binds the slice to a local variable
|
||||
first, so the assertion reads `len(speaks) != 12`. The claim was wrong, it was caught by reading the
|
||||
file rather than by grepping it, and the shape of the error is the same one issue 113 recorded: a
|
||||
negative search result read as a fact about the world.
|
||||
|
||||
## The argument the report asked for already exists, in a test comment
|
||||
|
||||
The report asked why a container rather than a supervised process, and said the reasoning was not
|
||||
written down. It is — in `declaration_test.go`, as the eleventh shape's paragraph:
|
||||
|
||||
> Running code of one's own meant a `container` and therefore an image; running a script meant a
|
||||
> `service` and a unit somebody else had to install. One intent — run this and keep it running —
|
||||
> expressed two unrelated ways, with the hosting chosen before anything could be declared. […] It
|
||||
> is a full-host shape rather than a portable one: it needs a process supervisor to install into.
|
||||
> It does NOT need a container runtime, which is the point — only software that genuinely needs
|
||||
> isolation asks for a container.
|
||||
|
||||
That is a decision's Context and Consequences, in a Go comment, in another repository. Nothing in
|
||||
`02-DECISIONS/` contains it. `TypeProcess`'s own doc comment adds the rest — that three modes beat
|
||||
three kinds, and that a first draft added a `daemon` for the long-running case alone.
|
||||
|
||||
## The catalogue is containers, and the one exception is the reference module
|
||||
|
||||
71 modules on `main`. Counting the `type` of every declared resource:
|
||||
|
||||
| `container` | `process` |
|
||||
|---|---|
|
||||
| 115 | **3** |
|
||||
|
||||
All three `process` resources are in **one** module: `showcase` — the module
|
||||
[`20-writing-a-module.md`](../../03-DESIGN/01-to-be/20-writing-a-module.md) is a worked guide for.
|
||||
|
||||
### And in that module, the tools do not run
|
||||
|
||||
`showcase` declares its migrate, server and reporting steps as `process`. Its fourth resource, the
|
||||
one for tools, is a **`container`** — and its image is the module's `helper` artifact, which the
|
||||
same manifest declares as `kind: upstream` from a bare distribution base. Its command is
|
||||
`sleep infinity`. It mounts the broker credential and sets the variable naming it, and runs nothing.
|
||||
|
||||
Meanwhile the module's `code` bundle declares six entrypoints. Three are run by the three `process`
|
||||
resources. The tools entrypoint and the provisioner entrypoint are **run by no resource in the
|
||||
manifest.**
|
||||
|
||||
Two consequences worth stating separately:
|
||||
|
||||
- **The worked guide does not match the module it documents.** The guide shows four `process`
|
||||
resources, the fourth being `{"id": "tools", "type": "process"}`. The module has three and a
|
||||
container.
|
||||
- **This is the condition ADR 0047 was written to end, in a new shape.** That record's Context says
|
||||
the conversion "produced tools and events that, as it stands, never execute," and its first
|
||||
Consequence is that they become runnable. In the reference module they do not execute again —
|
||||
not for want of a runtime this time, but because nothing declares one that runs them.
|
||||
|
||||
## The harness has no per-module boundary, and nothing refuses a second module
|
||||
|
||||
This is where ADR 0047's isolation argument is load-bearing, so it was checked rather than assumed.
|
||||
|
||||
- `mesh-sdk` `src/tools/index.ts`: `serveTools` iterates `collectTools()` over a module-level
|
||||
registration array and serves **every registered module's** tools over the **one** `broker` it
|
||||
was handed.
|
||||
- `mesh-tools` `src/main.ts`: the modules to load come from one variable as a **comma-separated
|
||||
list**, and the runtime sets its module and node identity from the **single** credential.
|
||||
- `mesh-sdk` `src/events/index.ts`: an emitted event's `x-source` is stamped from that single
|
||||
module identity.
|
||||
|
||||
Put together: load two modules into one runtime and everything the second emits is attributed to
|
||||
the first, because there is one credential and the identity comes from it. That is precisely the
|
||||
failure ADR 0047 predicted — "able to emit as any of them" — reached by a different route, since
|
||||
the credential is correct and there is only one of it for two modules. **Nothing in either
|
||||
repository refuses the second module**, and no test asserts that a runtime serves one.
|
||||
|
||||
### Ruled out, in fairness to the implementation
|
||||
|
||||
- **The serving key conforms.** ADR 0047 replaced a single `tools.invoke` dispatch with a per-tool
|
||||
key, and the SDK does that: a tool is served on `<module>.<tool>` with the account scoped
|
||||
`serve.<module>.*`. The superseded `tools.invoke` survives only in **prose** — the doc comment
|
||||
directly above the conforming code, and the `mesh-tools` README, which also describes the runtime
|
||||
as per-node. The code is ahead of its own documentation.
|
||||
- **The credential shape conforms.** The sealed per-module credential file is preferred in code, and
|
||||
the plain URL is documented as the bootstrap case before a module has an account — not the
|
||||
ordinary path.
|
||||
|
||||
So the account is the right shape and the key is the right shape. It is the **process boundary**
|
||||
that is declared nowhere and enforced by nothing.
|
||||
|
||||
## An unmerged report already asks the narrow version of this
|
||||
|
||||
Branch `issue/113-controller-container-or-process`, one commit, 2026-09-24, adds a report titled
|
||||
**"Should the controller run as a container, or as a process the host supervises directly?"** with
|
||||
`located-in: [mesh-controller module.json, mesh-host internal/apply]`. Its observation is that the
|
||||
controller is declared a `container` with `network: host` — so container network isolation, the
|
||||
property that resource type usually buys, is not in use — and it asks what `type: container` buys
|
||||
that `type: process` would not.
|
||||
|
||||
It was unmerged and numbered 113, which is taken. A sibling branch,
|
||||
`issue/113-record-the-repin-and-fold-114`, is why `114` was free.
|
||||
|
||||
**That report and this one are the instance and the general condition**, and they do not conflict:
|
||||
it asks about one module that is not a code-carrying sidecar at all, and reaches the same question
|
||||
from the opposite end. So it lands in this change as
|
||||
[issue 114](../114-should-the-controller-be-a-container-or-a-process/00-report.md), its commit and
|
||||
authorship intact, with a section pointing here — rather than being folded in and losing the
|
||||
`network: host` observation, which is its own and is not reproduced above.
|
||||
|
||||
This diagnosis answers its first open question. The host's `process` shape does support what
|
||||
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) describes for the host's own launcher — the
|
||||
unit, the timer, restart, and run-to-completion gating are implemented and tested — so that report
|
||||
does not need host-side work before it can be decided.
|
||||
|
||||
## What is located, and what is not
|
||||
|
||||
**Located — and it is not a code defect.** The implementation and the design layer agree with each
|
||||
other; the **decision record is what is missing**, and the accepted record that occupies its place
|
||||
says the other thing. ADR 0047 is `accepted`, cited by the module protocol, and unsuperseded, while
|
||||
the host it describes has had a purpose-built shape for a module's own code since the eleventh
|
||||
vocabulary entry.
|
||||
|
||||
| Owner | What is theirs |
|
||||
|---|---|
|
||||
| `hq` | the missing record for the `process` shape; ADR 0047 left standing; the worked guide that does not match the module |
|
||||
| `mesh-catalog modules/showcase` | tools and provisioner entrypoints that no resource runs; a tools container that sleeps |
|
||||
| `mesh-sdk src/tools/index.ts` | several modules served over one credential, unrefused and untested; a doc comment describing a superseded dispatch |
|
||||
| `mesh-tools` | a README describing a per-node multi-module runtime the code no longer prefers |
|
||||
|
||||
**Not located, and deliberately open:** whether `process` or `container` is *right* for a module's
|
||||
own code. This diagnosis establishes that the question was answered in practice and never recorded
|
||||
— not which answer is correct. The arguments on both sides now exist in writing; they exist in a
|
||||
test comment and a proposed design document, and one of them contradicts an accepted decision.
|
||||
|
||||
## What would close it
|
||||
|
||||
1. A decision record for the `process` shape, carrying the argument currently in
|
||||
`declaration_test.go`, and saying what becomes of ADR 0047 — superseded in whole, or in the part
|
||||
that names a container.
|
||||
2. `18-building-a-module.md` and `20-writing-a-module.md` naming that record in `decisions:`, and
|
||||
the worked manifest agreeing with the module.
|
||||
3. The eleventh shape's paragraph in the vocabulary test naming a decision, like the other three.
|
||||
4. **How the rule is checked, since a rule states how it is checked:** every shape in the host's
|
||||
vocabulary names a decision, asserted where the count is already asserted — which turns "no
|
||||
design without a decision" into something stronger than "no design without *a* decision" for
|
||||
the one vocabulary where each entry is a security decision.
|
||||
5. Whether a runtime may serve more than one module answered either way, and asserted — a refusal
|
||||
if not, a test that two modules' events keep their own source if so.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-09-26
|
||||
located-in: [mesh-sdk src/provisioner, mesh-catalog modules/redis]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 120 — A provisioner remembers what it did, not what is there
|
||||
|
||||
## What was observed
|
||||
|
||||
The provisioner harness every provider is built on keeps, in memory, a hash of what it last applied
|
||||
for each consumer: the login, the password and the values. On each pass it skips a consumer whose
|
||||
hash has not changed. It never asks the backend whether what it made is still there.
|
||||
|
||||
The cache module shows what that allows. Its server is configured with a password and a data
|
||||
directory, and **no ACL file**. So the per-consumer ACL users its provisioner creates exist only in
|
||||
the server's memory. The server and the provisioner run in separate containers:
|
||||
|
||||
1. the provisioner creates an ACL user for each consumer, and records it as applied;
|
||||
2. the server restarts, for an upgrade or a crash, and comes back with no consumer users;
|
||||
3. the provisioner, still running, sees nothing changed in what it receives, and does nothing;
|
||||
4. every consumer of the cache fails to authenticate, and **nothing reports it**. The provisioner's
|
||||
log is quiet, and the mesh's status is green.
|
||||
|
||||
The consumers recover only when the provisioner itself restarts, because its memory is then empty.
|
||||
Rotating the cache's administrative password happens to cover it, because that file is mounted into
|
||||
the provisioner too and recreates it. Nothing else does.
|
||||
|
||||
Evidence, from the catalogue's and the SDK's main branches: the harness's reconcile loop (`applied`,
|
||||
keyed by login, compared by hash before `create`), and the cache module's rendered configuration,
|
||||
which names no ACL file. Found during research 016, how a credential can be rotated, proposed
|
||||
alongside to-be 27.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
The cache is the case where the backend forgets on its own. The same gap opens whenever a backend
|
||||
loses what was provisioned while the provisioner keeps running: a store restored from a backup taken
|
||||
before a consumer was added, a login removed by hand, a server recreated on an empty data directory.
|
||||
In each of them the provisioner reports that everything is applied, because it compares against its
|
||||
own memory and not against the backend.
|
||||
|
||||
The harness's other half has the same shape. A consumer's contribution that disappears while the
|
||||
provisioner is down is never removed, because only logins the running process applied are candidates
|
||||
for removal. What the mesh wants and what the backend holds can drift in both directions, and the
|
||||
harness sees neither.
|
||||
|
||||
This is the design permitting a silent failure. *A provider makes what its consumers require true*
|
||||
is stated in [to-be 13](../../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md), and nothing
|
||||
checks it after the first pass.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Should the harness check each consumer's credential against the backend on every pass, or
|
||||
periodically, instead of trusting its memory? Most adapters' `create` is already idempotent, so the
|
||||
cheapest fix may be to drop the hash short-cut and apply every pass. What does that cost for a
|
||||
provider that recreates an access key on every create, as the object store does?
|
||||
- Should the cache keep its users in an ACL file, so a restart does not lose them? That fixes this
|
||||
instance and leaves the gap for the others.
|
||||
- Where does the record of what was applied live, if not in memory? ADR 0114, still
|
||||
proposed, puts rotation state with the vault. The same place may answer this.
|
||||
Reference in New Issue
Block a user