Issue 117: a module's own code is a container in one record and a process in another #110
@@ -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,193 @@
|
||||
# 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 is unmerged and numbered 113, which is taken. A sibling branch,
|
||||
`issue/113-record-the-repin-and-fold-114`, is why `114` is 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.
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user