mesh/merge-gate pass: builds mesh-sdk, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/asks-answered-on-any-channel checking: 3 of 4 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
155 lines
9.3 KiB
Markdown
155 lines
9.3 KiB
Markdown
# 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.
|
|
|
|
#### Taking an event — the one rule
|
|
|
|
A module's handler (`on(pattern, handler)` here, `stdio.Subscribe` in Go, the same in every language)
|
|
**takes an event by returning, and asks for it again by throwing** (Go: returning an error). Nothing
|
|
else counts: what a handler returns is not read.
|
|
|
|
On the wire, the runtime hands the bundle `mesh/event` with the envelope and acknowledges the event on
|
|
the bus when the answer is **anything but a JSON-RPC error** — `{}` (what the SDKs send), `null`, or no
|
|
`result` at all. An `error` answer, no answer within the runtime's event timeout (two minutes), or the
|
|
bundle exiting leaves it unacknowledged: it is offered again after five seconds, up to the module
|
|
consumer's maximum deliveries, after which the bus gives it up and the controller raises a condition
|
|
(`max-deliveries`). The runtime's line for an event not taken quotes a handler's error as the
|
|
handler's own words, so it is never mistaken for the runtime's.
|
|
|
|
So, for a handler:
|
|
|
|
- **Throw only when the work was not done** — the service refused, was unreachable, the store failed.
|
|
Never for something that went right but answered oddly: an API that answers an empty body is a
|
|
success, and reading it as JSON is a throw that has every event offered five times, its work done
|
|
five times, and a false alarm raised (novox/hq issue 276).
|
|
- **Delivery is at-least-once**, so the work must be safe to do twice, or the handler skips an event
|
|
whose `x-event-id` (`event.id`) it already did — remembering it only once the work succeeded, so one
|
|
that failed is done when it is offered again.
|
|
- An event no handler matches is answered at once and taken.
|
|
- A subscribe the runtime refused leaves no handler behind, so asking again until it is taken hands
|
|
each event to the handler once.
|
|
|
|
Checked by the SDKs' stdio tests (TypeScript `test/stdio.test.ts`, Go `go/stdio_test.go`) and the
|
|
runtime's (`node-tools/internal/launch`, mesh-tools).
|
|
|
|
### `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 `<module>.<name>`): `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).
|
|
|
|
### `seats` and `asks` — asking a person, answering on a channel (Go)
|
|
A seat may name its caller or its kind (novox/hq ADR 0259): `stdio.SeatPublish` submits an accept or says
|
|
an event under the module's own name or kind, `stdio.SeatTake` takes a held seat's work, `stdio.SeatProve`
|
|
and `stdio.SeatAnswer` carry a proof (a code typed by a person, by request and reply, never kept),
|
|
`stdio.SeatRecord` reads a record kept for the module, and `State(...).Create` and `.Update` write by
|
|
compare-and-set. The runtime does each only as far as the module's membership lists. `go/asks` is the
|
|
contract on top: the `Ask` an asker publishes and the checks it is held to, the `Warrant` it acts on
|
|
(`For`, `Says`), and the `Message`, `Choice`, `LinkAsked` and `Code` a channel takes and says, with the
|
|
subjects each travels on.
|
|
`go/asker` is an asking module's client (ADR 0259 §10): `Ask` keeps an ask in the module's own state and then
|
|
publishes it under the module's name; `Cancel`; `Where` reads its state from the router's record; `Listen` and
|
|
`Take` hand the module each warrant once — marked acted on by compare-and-set before the module acts, and only
|
|
for an ask it made, an option it offered, at that option's level, before it expired. A lost compare-and-set is
|
|
`stdio.ErrChanged`, told by the runtime's error code.
|
|
|
|
### `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<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/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
|