jschoubben f335bfb9e7 events: modules log activity to the broker, any module reacts
The lighter sibling of provisioning — 1:many and broadcast, no credential,
just the broker's topic routing. A thin, audit-ready surface over the
broker's publish/subscribe:

- emit(type, body): publishes an Event carrying who emitted it (MESH_MODULE),
  on which node (MESH_NODE) and when (ISO timestamp) — so a listener can
  build a real audit trail.
- on(pattern, handler): react to events by topic pattern. The audit logger
  is just on("#", ...).

Tested: a module emits; a targeted listener (module.umami.#) hears only its
events, the audit sink (#) hears every module's, and the metadata audit
needs is present.

The declared side — a manifest's emits/consumes, so the mesh knows the
event graph — and the audit-logger module are the next pieces.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-03 23:28:38 +02:00

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 0044). 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 0044 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 0044.)
  • Delivery machinery — build executor, bundler, dependency resolver, artifact manager, feature handlers → mesh-control. 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/0044-what-the-sdk-holds-and-refuses.md — the stability rule this repository is
  • 02-DECISIONS/0045-what-a-module-is.md — a module, and why its per-module code lives in the module
S
Description
Novox Mesh — the stable spine a module's own code builds against. Tool harness, provisioner reconcile loop, broker/event contract, runtime contracts, sealing. Holds nothing per-module (ADR 0044).
Readme
103 KiB
Languages
TypeScript 100%