Stand up mesh-sdk — the stable spine a module builds against
Per novox/hq ADR 0044/0045: the sdk holds only what rarely changes and is shared across modules; per-module code (a client, tool impls, a create-a-resource adapter) lives in the module. Five areas, real and tested: - contracts: the runtime shapes module code touches (grant, credential, a mesh Interface, tool + envelope types) — not the manifest schema, which the control plane owns. - provisioner: the reconcile harness every provider shares (watch grants, create via the module's adapter, seal + write the credential, remove on withdrawal). A module writes only the adapter. - tools: registerModuleTools + collectTools — the serving harness; tools and their client live in the module. - messaging: the Broker/Envelope/event contract over the mesh broker; the concrete binding is provided by the hosting runtime. - primitives: AES-256-GCM seal/unseal, semver, resolved-env access. Compiles (tsc, NodeNext) and passes tests: sealing round-trip + wrong-key rejection, semver, tool registration (a thrower is skipped not fatal), and the provisioner creating then removing a sealed grant. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# 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](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 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:
|
||||
|
||||
```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<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`](https://git.novox.be/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
|
||||
Reference in New Issue
Block a user