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:
2026-09-03 23:01:59 +02:00
commit a19a2f5cf0
12 changed files with 639 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
// The reconcile harness every provider shares. The loop below is identical for postgres, redis,
// minio and umami: read the grants the control plane has written, bring each requested resource to
// existence through the provider's adapter, seal and hand back the credential, and remove what is
// no longer requested. A module writes ONLY the adapter — the per-service half — which is why this
// lives in the sdk and the adapter lives in the module (novox/hq ADR 0044).
import { readdir, readFile, writeFile, rm } from "node:fs/promises";
import { join } from "node:path";
import type { Grant, Credential } from "../contracts/index.js";
import { seal } from "../primitives/index.js";
/** What a provider implements — the only per-service code. It is handed a Grant and creates (or
* removes) the resource in its own software, returning the credential a consumer receives. */
export interface Adapter {
create(grant: Grant): Promise<Credential>;
remove(grant: Grant): Promise<void>;
}
export interface ProvisionerOptions {
/** Directory the control plane writes grant requests into and the harness writes credentials to.
* Defaults to $GRANTS. */
grants?: string;
/** Passphrase a credential is sealed to before it is written. Defaults to $MESH_SEAL_KEY. */
sealKey?: string;
/** Reconcile interval in ms. Defaults to 5000. */
everyMs?: number;
}
export type { Grant, Credential };
/**
* Run the reconcile loop for one provided resource. Returns a stop function. Never throws for a
* single bad grant — it logs and keeps converging, because one consumer's failure must not stop
* the others' provisioning.
*/
export function runProvisioner(resource: string, adapter: Adapter, opts: ProvisionerOptions = {}): () => void {
const dir = opts.grants ?? envOrThrow("GRANTS");
const sealKey = opts.sealKey ?? envOrThrow("MESH_SEAL_KEY");
const everyMs = opts.everyMs ?? 5000;
const applied = new Map<string, string>(); // consumer -> hash of the grant last applied
let stopped = false;
async function reconcile(): Promise<void> {
const requested = await readGrants(dir, resource);
const wantById = new Map(requested.map((g) => [g.consumer, g]));
// Create or update anything requested whose grant has changed.
for (const grant of requested) {
const h = hash(grant);
if (applied.get(grant.consumer) === h) continue;
try {
const cred = await adapter.create(grant);
await writeSealedCredential(dir, grant, cred, sealKey);
applied.set(grant.consumer, h);
} catch (err) {
console.error(`[provisioner:${resource}] ${grant.consumer}: create failed, will retry: ${err}`);
}
}
// Remove anything applied that is no longer requested. Only the harness's own outputs are
// touched — the credential file it wrote — never anything it did not create.
for (const consumer of [...applied.keys()]) {
if (wantById.has(consumer)) continue;
const grant = lastGrant(dir, resource, consumer);
try {
await adapter.remove(grant);
await rm(credentialPath(dir, resource, consumer), { force: true });
applied.delete(consumer);
} catch (err) {
console.error(`[provisioner:${resource}] ${consumer}: remove failed, will retry: ${err}`);
}
}
}
const tick = async (): Promise<void> => {
if (stopped) return;
try {
await reconcile();
} catch (err) {
console.error(`[provisioner:${resource}] reconcile error: ${err}`);
}
if (!stopped) setTimeout(() => void tick(), everyMs);
};
void tick();
return () => {
stopped = true;
};
}
// --- grant/credential files ---
async function readGrants(dir: string, resource: string): Promise<Grant[]> {
let names: string[];
try {
names = await readdir(dir);
} catch {
return [];
}
const out: Grant[] = [];
for (const name of names) {
if (!name.endsWith(".grant.json")) continue;
try {
const raw = await readFile(join(dir, name), "utf8");
const g = JSON.parse(raw) as Grant;
if (g.resource === resource) out.push(g);
} catch (err) {
console.error(`[provisioner:${resource}] unreadable grant ${name}: ${err}`);
}
}
return out;
}
function credentialPath(dir: string, resource: string, consumer: string): string {
return join(dir, `${consumer}.${resource}.credential`);
}
async function writeSealedCredential(dir: string, grant: Grant, cred: Credential, key: string): Promise<void> {
const body = JSON.stringify({ resource: grant.resource, consumer: grant.consumer, fields: cred.fields });
await writeFile(credentialPath(dir, grant.resource, grant.consumer), seal(body, key), { mode: 0o600 });
}
function lastGrant(dir: string, resource: string, consumer: string): Grant {
// For removal the harness needs a Grant to hand the adapter; the identity is enough to act on.
return { resource, consumer, node: "", values: {} };
}
// --- helpers ---
function hash(g: Grant): string {
return JSON.stringify([g.resource, g.consumer, g.node, g.values]);
}
function envOrThrow(name: string): string {
const v = process.env[name];
if (!v) throw new Error(`${name} is not set — the provisioner cannot run without it`);
return v;
}