diff --git a/04-ISSUES/114-should-the-controller-be-a-container-or-a-process/00-report.md b/04-ISSUES/114-should-the-controller-be-a-container-or-a-process/00-report.md new file mode 100644 index 0000000..6b03b96 --- /dev/null +++ b/04-ISSUES/114-should-the-controller-be-a-container-or-a-process/00-report.md @@ -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 ` — 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. diff --git a/04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md b/04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md new file mode 100644 index 0000000..1ce0e9c --- /dev/null +++ b/04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md @@ -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. diff --git a/04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/01-diagnosis.md b/04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/01-diagnosis.md new file mode 100644 index 0000000..3ec972e --- /dev/null +++ b/04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/01-diagnosis.md @@ -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 `.` with the account scoped + `serve..*`. 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.