Issue 009 (fixed) + Issue 008 (resolved via ADR 0053) — module-runtime config & provider contract #21

Merged
jschoubben merged 21 commits from worktree-issue-provider-seal-key into main 2026-09-05 01:02:34 +00:00
Owner

Two gaps found building out the module runtime (ADR 0052), both now fixed and proven in the lab.

009 — a settings change did not restart a container-hosted runtime. Containers are recreated only on a spec change, and a mounted file's content is not part of the spec. Fixed: mesh-host PR novox/mesh-host#2 gives containers restart-on (mirroring services); the catalogue's runtimes declare restart-on: ["runtime-config"]. Proven: a running grafana runtime picked up a token change on the next push.

008 — a provider's provisioner sealed with a key the mesh could not deliver, and did not need to. A cross-repo trace showed the sdk's symmetric-seal provisioner was orphaned and contradicted the mesh, which already mints one password per consumer/provider pair and hands the provider its copy. Resolved via ADR 0053 (accepted): the sdk harness now reconciles the mesh's receives contributions and creates each resource under the mesh-derived login with the mesh-minted password; $MESH_SEAL_KEY, the symmetric seal()/unseal() primitive, and the .grant.json/.credential files are removed. The four adapters (redis, postgres, minio, umami) are re-pointed at create({as,password,values})/remove({as}). Proven: provider-uses-mesh-credential is green — redis creates the consumer's login with the mesh's password, the consumer authenticates (PONG), no seal key anywhere. The contract is one place (the harness), so every future provider inherits it.

Scope boundary (in the ADR): this covers credential provisions (a mesh-minted secret). Data provisions — umami's analytics, where the consumer needs a provider-generated siteId back — are a different shape needing a return path the mesh does not have; left to a separate decision.

Code across repos: mesh-sdk events/tool-per-key, mesh-catalog events/audit-logger-assigned, mesh-lab events/audit-e2e, mesh-host#2.

Contents: 02-DECISIONS/0053-*, 04-ISSUES/008-*, 04-ISSUES/009-*.

https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF

Two gaps found building out the module runtime (ADR 0052), both now fixed and proven in the lab. **009 — a settings change did not restart a container-hosted runtime.** Containers are recreated only on a spec change, and a mounted file's content is not part of the spec. **Fixed:** mesh-host PR novox/mesh-host#2 gives containers `restart-on` (mirroring services); the catalogue's runtimes declare `restart-on: ["runtime-config"]`. Proven: a running grafana runtime picked up a token change on the next push. **008 — a provider's provisioner sealed with a key the mesh could not deliver, and did not need to.** A cross-repo trace showed the sdk's symmetric-seal provisioner was orphaned and contradicted the mesh, which already mints one password per consumer/provider pair and hands the provider its copy. **Resolved via ADR 0053 (accepted):** the sdk harness now reconciles the mesh's `receives` contributions and creates each resource under the mesh-derived login with the mesh-minted password; `$MESH_SEAL_KEY`, the symmetric `seal()`/`unseal()` primitive, and the `.grant.json`/`.credential` files are removed. The four adapters (redis, postgres, minio, umami) are re-pointed at `create({as,password,values})`/`remove({as})`. Proven: `provider-uses-mesh-credential` is green — redis creates the consumer's login with the mesh's password, the consumer authenticates (PONG), no seal key anywhere. The contract is one place (the harness), so every future provider inherits it. Scope boundary (in the ADR): this covers **credential** provisions (a mesh-minted secret). **Data** provisions — umami's `analytics`, where the consumer needs a provider-*generated* siteId back — are a different shape needing a return path the mesh does not have; left to a separate decision. Code across repos: mesh-sdk `events/tool-per-key`, mesh-catalog `events/audit-logger-assigned`, mesh-lab `events/audit-e2e`, mesh-host#2. Contents: `02-DECISIONS/0053-*`, `04-ISSUES/008-*`, `04-ISSUES/009-*`. https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben added 1 commit 2026-09-04 20:31:00 +00:00
Found building the module-runtime vertical slice: a provider's provisioner
requires a seal key it has no way to receive, and the consumer no way to obtain
the matching one. The runtime cannot come up as delivered.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben added 1 commit 2026-09-04 21:09:10 +00:00
Found rolling the runtime out to the catalogue: a module's runtime reads its
settings-merged config file once at start, but a container is only recreated on a
spec change, and file content is not part of the spec. So updating settings
re-renders the file and nothing re-reads it — ADR 0051's "on the fly" holds only
for config set before first start. Services have restart-on; containers do not.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben changed title from Issue 008 — a provider runtime has no seal key the mesh can deliver to Issues 008 & 009 — two gaps in the module-runtime config/secret delivery 2026-09-04 21:09:30 +00:00
jschoubben added 1 commit 2026-09-04 21:40:38 +00:00
A cross-repo trace showed nothing writes the provisioner's grant-request files,
nothing reads its sealed credentials, and no consumer unseals — while the mesh
already mints and delivers provider/consumer credentials asymmetrically with no
shared key. The fix is to drop the symmetric seal and have providers consume the
mesh-minted password, a breaking provider-contract change that wants an ADR.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben added 1 commit 2026-09-04 22:06:57 +00:00
Issue 008's trace confirmed the premise in control-plane code: the mesh already
mints one password per consumer/provider pair and delivers the provider its copy
(SecretFor/SecretsFrom/grantsFor -> Grant.Sealed; the receives contribution carries
As + Secret). The provisioner's symmetric seal is an orphaned, contradictory second
model. ADR 0053 corrects the provider contract in one place (the sdk harness):
providers create the resource with the mesh-supplied login and password and drop
seal/key/return entirely. Reframe 008 as contract-first (every provider, not four).

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben changed title from Issues 008 & 009 — two gaps in the module-runtime config/secret delivery to Issues 008 & 009 + ADR 0053 — the module-runtime config/secret gaps and the provider-contract fix 2026-09-04 22:07:12 +00:00
jschoubben added 1 commit 2026-09-04 22:27:58 +00:00
ADR 0053 accepted; adds the scope boundary the umami rework surfaced (credential
provisions vs data provisions — analytics' generated siteId return is left to a
separate decision) and records the lab proof. Issue 008 marked resolved: the sdk
harness and the four adapters are reworked, the symmetric seal removed, and
provider-uses-mesh-credential is green.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben changed title from Issues 008 & 009 + ADR 0053 — the module-runtime config/secret gaps and the provider-contract fix to Issue 009 (fixed) + Issue 008 (resolved via ADR 0053) — module-runtime config & provider contract 2026-09-04 22:34:13 +00:00
jschoubben added 1 commit 2026-09-04 23:58:12 +00:00
Found doing the per-backend provider e2e: redis and postgres accept the mesh's `as`
(mesh_<node>_<module>) verbatim, but minio's S3 access key is capped at 20 chars and
`as` is 22, so the provisioner cannot create the service account. `as` is doing two
jobs — a stable identity the two ends agree on, and a literal identifier a backend
must accept — and those are not always the same string.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben added 1 commit 2026-09-05 00:17:48 +00:00
Sketches the options for issue 010: the mesh's identityLimit (63, postgres's) is not
the shortest among the backends the derived name reaches — S3's is 20 — so CheckIdentity
lets an over-long access key through and minio fails at provision time. Options: bound
by the true minimum and refuse at assignment (recommended, with a compact fallback held
in reserve), per-interface bounds, or a provider-generated identity (rejected — breaks
"the mesh says the identity once"). Links issue 010 to it.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben added 1 commit 2026-09-05 00:30:13 +00:00
Implementing A (bound the identity at 20) revealed the readable budget is node+module
<= 14 chars — so tight that the catalogue's own test names (workstation+keycloak, 25)
compact to an opaque hash. B's fallback would fire for the common case, not the rare
overflow, inverting A+B into mostly-opaque identities. Option E — an optional short
slug a module/node declares, preferred over the cleaned name — is the escape hatch B
wanted to be without the opacity: legible because a person chose it, and it makes an
early refusal palatable (refuse on the slug field, not the machine's name). B dropped;
E recommended over a bound of 20, composing with C later if needed.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben added 1 commit 2026-09-05 00:52:05 +00:00
ADR 0054 accepted with option E (a declared slug). Issue 010 resolved: the login fits
via the slug, and the minted secret shrinks to 40 chars for S3's secret-key limit —
both halves of an S3 credential now fit the tightest backend.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben added 12 commits 2026-09-05 01:01:08 +00:00
Supersedes ADR 0030's 'types, not behaviour' line for mesh-sdk. The
boundary is change-frequency, not kind: the SDK holds the stable spine
(tool-serving harness, messaging/event framework, contracts, core
primitives) and refuses per-module clients, per-module tool code, and
anything volatile — because those are what turned hal/sdk into constant
maintenance and made every edit rebuild every module.

States the rule (frequent AND cascading is the disease), why the root
cause was intra-module feature-sharing leaking into inter-module
coupling, and where per-module shared code lives instead (in the
module — a shared file, or a module-local sdk for the few large ones).
Updates repos.md's canonical mesh-sdk description to match; leaves 0030
untouched (immutable).

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
A module is one self-contained piece of software the mesh installs and
manages; the software is its identity, and capabilities/seats/provisions
are the relationships between modules, not what a module is. Records the
three relationships (shared seat, exclusive seat, provide/require), that
interfaces are mesh-owned and providers adapt to them, and the naming
rule: draw the interface at the consumer's real coupling — neutral where
the coupling is thin (analytics), protocol-scoped where the consumer
speaks a protocol (postgres/mssql/mongodb), never false genericity.

Supersedes 0017 (domain grouping — wrong axis), refines 0002, generalises
0027's protocol-not-product rule.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
A module emits and consumes events, both declared (emits/consumes),
parallel to provides/requires. Events are 1:many, broadcast, credential-
free — no provisioner, just the broker's topic routing — so most inter-
module reaction should be an event, not a provision. Every event carries
source/node/time so it is auditable; the audit logger is just a module
consuming '#', no privilege. A consumes for an event nothing emits is a
dangling edge and refused, like requires. One per-node runtime serves
tools, provisioning and events alike.

Extends ADR 0045; builds on ADR 0001 (the broker) and 0044 (emit/on are
stable sdk surface; the binding and runtime are not).

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
The wire contract ADR 0046 left open: two topic exchanges (mesh.events,
mesh.rpc, kept apart so # is a clean audit); the routing key as the event
type namespaced by origin (module.*, mesh.*, node.*); metadata in AMQP
headers (required x-event-id/x-source/x-node/x-time/content-type; optional
x-causation-id/x-schema; unknown x- headers ignored) with the body only the
payload; persistent messages; per-consumer durable dead-lettered queues
with prefetch; at-least-once with idempotent consumers (no false exactly-
once). The precedent is ADR 0043 for declarations.

Supersedes the sdk's first cut (metadata in body -> headers); that and the
queue config are code to align in mesh-sdk and mesh-tools.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
Events (0046) and their wire (0047) left open how a module reaches the
broker. The code has no generic module broker-account: only node and
builder scopes exist, so emits/consumes are enforced by nothing — a
manifest declaring a scope the broker does not draw (04-ISSUES/003).

Decides: on assign, a module gets a broker account whose permissions ARE
the manifest — read on mesh.events + its own queue bound to consumes;
write to mesh.events under module.<self>.* only; nothing else. Consuming
'#' is a deliberate, auditable grant. The account is what makes the
declaration a rule the broker enforces, not a comment.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
0049: a public name is provisioned like any capability — a module requires
public-dns and contributes its host; a neutral interface answered by
registrar-scoped providers (cloudflare-dns, route53-dns) that create/remove
the record pointing the name at the mesh's public ingress. Pairs with route
(the proxy) and a public cert (the proxy's ACME).

0050: answers the firewall question. The firewall is NOT a provider like the
proxy — it is a machine's own filter, derived by the host as the sum of what
its modules declare they listen on, with 'from' the whole of public-vs-internal.
Enforced both ways, unknown keys refused — closing 04-ISSUES/003. A public
service is exposed through the proxy (listens from:mesh + requires route), not
by opening its own port.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
The manifest refuses unknown keys (DisallowUnknownFields), 'from' is the field
that scopes a port and it is rendered to nftables (AsNftables), and the firewall
module applies the rule set. The chain from a declared scope to a dropped packet
is closed. Amended-design: ADR 0050.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
A module is assigned to a node (there is no mesh assignment; 'mesh' is a scope).
The manifest is what the module IS, plus defaults; the configurable values are
settings, carried by the assignment — per-node or mesh-wide, applied at
resolution, changeable live (what a meshboard edits). Extends settings from a
config file's content to the manifest fields marked settable: foremost
listens.from (postgres from:mesh by default, from:anywhere per node — the
firewall follows), and a provider's own config (a registrar's zone/domain/
ingress). Static config in a manifest is config in the wrong place: it cannot
vary per node and cannot change without a rebuild.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
The runtime-model gap the review found. A module with tools or events runs one
container — the tool runtime carrying its code — holding the one scoped account
ADR 0048 gave it. A node-wide runtime can't: it would hold the union of every
module's permissions, the isolation 0048 draws. So per-module: one module, one
process, one account. Tools served per key (serve.<tool>) so a caller names a
tool and only its module answers (superseding a shared tools.invoke); events in
the same process under the same account; the runtime image is the tool runtime
plus the module's code (the audit-logger's shape, made the rule). A plain
service module runs no such process. A provider's provisioner is a runtime too —
which is why a provisioner that emits must carry a broker credential or not emit.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
Brings the independent ADR branches (0044-0052) onto one branch so hq lands as a
single MR, and ratifies the five that were still proposed — 0017, and 0049-0052,
which are implemented and green in the lab. With 0053/0054 already accepted here, the
whole ADR chain 0044-0054 is accepted on this branch.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben merged commit 36936dc7a3 into main 2026-09-05 01:02:34 +00:00
jschoubben deleted branch worktree-issue-provider-seal-key 2026-09-05 01:02:34 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/hq#21