Filed 2026-09-24 on a branch of its own and never merged, numbered 113, which is taken. 114 is free because a sibling branch folded it, so it takes that number and keeps its commit. Kept separate from issue 117 rather than folded into it. 117 asks the same question of every module and locates the missing decision; this asks it of the controller, where `network: host` means container network isolation — the property that resource type usually buys — is not in use. That observation is this report's own and is nowhere in 117, and folding would lose it. Its first open question is answered by 117's diagnosis and now says so: the host's `process` shape is built, applied and tested, restart and run-to-completion semantics included, so deciding this does not wait on host-side work.
12 KiB
Diagnosis — 117
Which trees were searched, 2026-09-25
Named first, because issue 113 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 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
containerand therefore an image; running a script meant aserviceand 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 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
processresources, 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-sdksrc/tools/index.ts:serveToolsiteratescollectTools()over a module-level registration array and serves every registered module's tools over the onebrokerit was handed.mesh-toolssrc/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-sdksrc/events/index.ts: an emitted event'sx-sourceis 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.invokedispatch with a per-tool key, and the SDK does that: a tool is served on<module>.<tool>with the account scopedserve.<module>.*. The supersededtools.invokesurvives only in prose — the doc comment directly above the conforming code, and themesh-toolsREADME, 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, 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 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
- A decision record for the
processshape, carrying the argument currently indeclaration_test.go, and saying what becomes of ADR 0047 — superseded in whole, or in the part that names a container. 18-building-a-module.mdand20-writing-a-module.mdnaming that record indecisions:, and the worked manifest agreeing with the module.- The eleventh shape's paragraph in the vocabulary test naming a decision, like the other three.
- 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.
- 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.