# 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](https://git.novox.be/novox/hq)). 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. **A consumer the mesh stops asking for is retired, not withdrawn whole** (novox/hq ADR 0230). ACTIVE becomes RETIRED only once the same set of consumers has gone unasked in five consecutive passes that read the file AND for ten minutes (`stableForMs`, `STABLE_FOR_MS`; a pass that cannot read the file starts both again). An adapter with `retire` disables the consumer's access, reversibly, and marks it; `reenable` (optional) undoes that before `create` when it is asked for again. **An adapter without `retire` is mark-only**: nothing in the backend changes — the consumer keeps its access — and the harness marks it "retired, access kept, needs deletion", says so loudly and announces it. **`remove` is never called to retire**; it is the delete of an adapter without `delete`, reached only through a person's `provisioner_delete`. A stable set of more than 3, or more than half of those held where more than one is held, is not retired without a person: the provider waits for `provisioner_retire_approve` or `provisioner_retire_reject`, and says `provisioner.retirement` (`waiting`) through the `announce` option. Asked for again, a retired consumer is created — re-enabled — at once. Given `module`, the four tools (`provisioner_retirement`, `provisioner_retire_approve`, `provisioner_retire_reject`, `provisioner_delete`) are registered for it; `provisionerTools(handle)` builds them otherwise. Additions and changes are never delayed. ### `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. ### `state` — a module's current state on the bus `state(name)` reaches a state the module declared it keeps (`state` in its manifest) or reads (`reads`, as `.`): `get`, `put`, `delete`, `keys`, and `watch`, which hands the current values and then every change. Through the runtime that launched the bundle, which refuses what the module did not declare. Current values, never history and never a secret (novox/hq ADR 0201). ### `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: ```ts 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: ```ts import { runProvisioner, Grant, Credential } from "@novox/mesh-sdk/provisioner"; runProvisioner("analytics", { async create(grant: Grant): Promise { /* create a umami site, return the grant */ }, async remove(grant: Grant): Promise { /* 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`](https://git.novox.be/novox/hq): - `02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md` — the stability rule this repository is - `02-DECISIONS/0040-what-a-module-is.md` — a module, and why its per-module code lives in the module