A check that hangs no longer stalls every consumer: it times out after 30s and counts as could-not-ask, and the rest of that pass is not asked. A consumer still not held after being applied again is checked at doubling intervals up to an hour, and said loudly, so an adapter whose create and holds disagree costs one re-apply an hour, not one a minute. The consumer's password is scrubbed from every error the harness logs.
mesh-sdk
The stable spine a Novox Mesh module's own code builds against — the tool it uses to plug into the mesh, and nothing that belongs to a specific module or to another tier.
It earns its place by rarely changing (novox/hq ADR 0039). The test for anything here: if editing it recompiles unrelated modules and it changes often, it does not belong. Per-module code (a service's API client, its tool implementations, its create-a-resource adapter) lives in the module, never here — that coupling is exactly what turned the old SDK into constant maintenance and made every edit rebuild every module.
What is in it
Five small areas, each a thing a module's tools/ or provisioner/ imports:
contracts — the runtime shapes module code touches
Not the manifest schema (the control plane owns and parses that, in Go). The shapes a module's
running code receives and returns: a provisioning grant and its credentials, the
mesh interface definitions (what an analytics grant contains, what an oidc-client grant
contains — the contract both a provider and a consumer conform to), and the tool and event types.
These change rarely and deliberately; when one does, a rebuild of everything is correct.
provisioner — the reconcile harness every provider shares
The loop is identical for postgres, redis, minio and umami: watch the grants directory, for each
requested grant call the provider's adapter, write the sealed credential, handle withdrawal, and
keep converging (watch). That loop lives here. A module provides only the adapter — "create
a database", "create a umami site" — which is the volatile, per-service half and belongs in the
module. This is the ADR 0039 line drawn through provisioning: stable loop here, per-service create
in the module.
tools — the tool-serving harness
registerModuleTools, the ToolDefinition type, and the registry/worker that loads a module's
tools and exposes them through the mesh's command surface. How a tool is declared and served is
settled; it does not change when an individual tool does. The tools themselves, and the API client
they call, live in the module.
messaging — the broker and event framework
The broker client (connect, request/reply, publish/subscribe), the event-consumer a module uses to react to mesh events, and the envelope/routing-key conventions. Transport primitives.
primitives — sealing, semver, resolved-env
Seal/unseal for secrets, semantic version comparison, and the accessor a module uses to read its own resolved environment.
What is NOT in it, and where it goes
- A module's API client and its tool implementations → in the module. (The Plex client, the
Umami client,
tools/plex.ts— all module-local. ADR 0039.) - Delivery machinery — build executor, bundler, dependency resolver, artifact manager, feature handlers → mesh-controller. It co-evolves with the pipeline.
- Host synchronisers — config-sync, ufw config, vhost generation, systemd, health checks → mesh-host's apply engine. The host applies these; they are not module code.
- Domain logic — tasks, workflows, agents, provider integrations → tier-2 contexts.
That is why this stays small: everything volatile has somewhere else to be.
Using it
A module's tools:
import { registerModuleTools, ToolDefinition } from "@novox/mesh-sdk/tools";
import { UmamiClient } from "../client.js"; // the client lives in the module, not here
registerModuleTools("umami", (env) => getUmamiTools(new UmamiClient(env.UMAMI_URL)));
A provider's provisioner:
import { runProvisioner, Grant, Credential } from "@novox/mesh-sdk/provisioner";
runProvisioner("analytics", {
async create(grant: Grant): Promise<Credential> { /* create a umami site, return the grant */ },
async remove(grant: Grant): Promise<void> { /* delete the site */ },
});
The adapter is the only thing the module writes; the watching, sealing and grant-file handling are the harness's.
Where the reasoning lives
Design and decisions are in novox/hq:
02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md— the stability rule this repository is02-DECISIONS/0040-what-a-module-is.md— a module, and why its per-module code lives in the module