Resync hq ADR references 0044-0054 -> 0039-0049 after the hq record reconciliation

This commit is contained in:
2026-09-05 12:44:17 +02:00
parent 4973b69786
commit 9b7817c8ca
8 changed files with 22 additions and 22 deletions
+5 -5
View File
@@ -3,7 +3,7 @@
The stable spine a Novox Mesh **module's own code builds against** — the tool it uses to plug 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. 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)). 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 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 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 create-a-resource adapter) lives **in the module**, never here — that coupling is exactly what
@@ -25,7 +25,7 @@ The loop is identical for postgres, redis, minio and umami: watch the grants dir
requested grant call the provider's adapter, write the sealed credential, handle withdrawal, and 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 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 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 module. This is the ADR 0039 line drawn through provisioning: stable loop here, per-service create
in the module. in the module.
### `tools` — the tool-serving harness ### `tools` — the tool-serving harness
@@ -45,7 +45,7 @@ own resolved environment.
## What is NOT in it, and where it goes ## 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 - **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.) Umami client, `tools/plex.ts` — all module-local. ADR 0039.)
- **Delivery machinery** — build executor, bundler, dependency resolver, artifact manager, feature - **Delivery machinery** — build executor, bundler, dependency resolver, artifact manager, feature
handlers → **mesh-control**. It co-evolves with the pipeline. handlers → **mesh-control**. It co-evolves with the pipeline.
- **Host synchronisers** — config-sync, ufw config, vhost generation, systemd, health checks → - **Host synchronisers** — config-sync, ufw config, vhost generation, systemd, health checks →
@@ -83,5 +83,5 @@ the harness's.
Design and decisions are in [`novox/hq`](https://git.novox.be/novox/hq): 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/0039-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 - `02-DECISIONS/0040-what-a-module-is.md` — a module, and why its per-module code lives in the module
+4 -4
View File
@@ -1,7 +1,7 @@
// The runtime shapes a module's own code touches — NOT the manifest schema, which the control // The runtime shapes a module's own code touches — NOT the manifest schema, which the control
// plane owns and parses (in Go). These are what a running module receives and returns: a grant // plane owns and parses (in Go). These are what a running module receives and returns: a grant
// and its credentials, the mesh interface a provider and consumer both conform to, and the tool // and its credentials, the mesh interface a provider and consumer both conform to, and the tool
// and event types. They change rarely and deliberately (novox/hq ADR 0044). // and event types. They change rarely and deliberately (novox/hq ADR 0039).
/** A request for one instance of a provided resource, addressed to a provider. */ /** A request for one instance of a provided resource, addressed to a provider. */
export interface Grant { export interface Grant {
@@ -21,7 +21,7 @@ export interface Credential {
} }
/** /**
* A mesh interface: the provider-neutral contract for a capability (novox/hq ADR 0045). Both a * A mesh interface: the provider-neutral contract for a capability (novox/hq ADR 0040). Both a
* provider (which adapts its software to it) and a consumer (which depends on it, never on a * provider (which adapts its software to it) and a consumer (which depends on it, never on a
* provider) conform. `spec` names what a consumer may contribute; `credential` names what it * provider) conform. `spec` names what a consumer may contribute; `credential` names what it
* receives. The interface is drawn at the consumer's real coupling: neutral where thin * receives. The interface is drawn at the consumer's real coupling: neutral where thin
@@ -45,7 +45,7 @@ export interface ToolDefinition {
} }
/** /**
* The metadata that rides an event as AMQP headers (novox/hq ADR 0047). An event's identity and * The metadata that rides an event as AMQP headers (novox/hq ADR 0042). An event's identity and
* provenance live here, not in the body, so a consumer — or the broker, or an audit tool — reads * provenance live here, not in the body, so a consumer — or the broker, or an audit tool — reads
* who/when/what without parsing the payload. An unknown `x-` header is ignored, not refused: an * who/when/what without parsing the payload. An unknown `x-` header is ignored, not refused: an
* event is observed by parties that need not all understand every header. * event is observed by parties that need not all understand every header.
@@ -70,7 +70,7 @@ export interface EventHeaders {
/** /**
* A message crossing the broker: a routing key and a JSON body, per-node addressed. For an event, * A message crossing the broker: a routing key and a JSON body, per-node addressed. For an event,
* `headers` carries the ADR 0047 metadata; plain request/reply transport leaves it absent. * `headers` carries the ADR 0042 metadata; plain request/reply transport leaves it absent.
*/ */
export interface Envelope<T = unknown> { export interface Envelope<T = unknown> {
readonly key: string; readonly key: string;
+3 -3
View File
@@ -4,7 +4,7 @@
// credential — just the broker's topic routing). Declared as emits/consumes on the manifest so the // credential — just the broker's topic routing). Declared as emits/consumes on the manifest so the
// mesh knows the event graph. // mesh knows the event graph.
// //
// Identity and provenance ride as AMQP headers, not in the body (novox/hq ADR 0047): a consumer — // Identity and provenance ride as AMQP headers, not in the body (novox/hq ADR 0042): a consumer —
// or the broker, or an audit tool — reads who/when/what without parsing the payload, and the body // or the broker, or an audit tool — reads who/when/what without parsing the payload, and the body
// is only the domain payload. This surface hides the header/exchange/queue mechanics; a module // is only the domain payload. This surface hides the header/exchange/queue mechanics; a module
// names a type and a body and never sees the wire. // names a type and a body and never sees the wire.
@@ -44,7 +44,7 @@ export interface EmitOptions {
/** /**
* Emit an event. Source and node come from the environment the runtime set for the module * Emit an event. Source and node come from the environment the runtime set for the module
* (MESH_MODULE, MESH_NODE), so a module names only the type and the body; the sdk stamps the * (MESH_MODULE, MESH_NODE), so a module names only the type and the body; the sdk stamps the
* ADR 0047 headers (id, source, node, time) and the runtime rides them on the broker. * ADR 0042 headers (id, source, node, time) and the runtime rides them on the broker.
*/ */
export async function emit<T>(type: string, body: T, opts: EmitOptions = {}): Promise<void> { export async function emit<T>(type: string, body: T, opts: EmitOptions = {}): Promise<void> {
const source = process.env.MESH_MODULE ?? "unknown"; const source = process.env.MESH_MODULE ?? "unknown";
@@ -72,7 +72,7 @@ export async function on<T>(pattern: string, handler: (event: Event<T>) => Promi
}); });
} }
/** Rebuild the Event a handler sees from a broker envelope's headers (ADR 0047) and body. */ /** Rebuild the Event a handler sees from a broker envelope's headers (ADR 0042) and body. */
function fromEnvelope<T>(env: Envelope<T>): Event<T> { function fromEnvelope<T>(env: Envelope<T>): Event<T> {
const h = env.headers ?? ({} as EventHeaders); const h = env.headers ?? ({} as EventHeaders);
return { return {
+1 -1
View File
@@ -1,4 +1,4 @@
// @novox/mesh-sdk — the stable spine a module's own code builds against (novox/hq ADR 0044). // @novox/mesh-sdk — the stable spine a module's own code builds against (novox/hq ADR 0039).
// //
// Import the area you need directly (`@novox/mesh-sdk/tools`, `/provisioner`, …) or the whole // Import the area you need directly (`@novox/mesh-sdk/tools`, `/provisioner`, …) or the whole
// surface from here. Everything specific to one module — its API client, its tool implementations, // surface from here. Everything specific to one module — its API client, its tool implementations,
+1 -1
View File
@@ -3,7 +3,7 @@
// //
// There was a symmetric seal()/unseal() here, for a provider to seal a credential to a key before // There was a symmetric seal()/unseal() here, for a provider to seal a credential to a key before
// writing it. It is gone: a provider is handed the credential the mesh minted and seals nothing // writing it. It is gone: a provider is handed the credential the mesh minted and seals nothing
// (novox/hq ADR 0053), the mesh's own secret delivery is asymmetric and belongs to the host, and // (novox/hq ADR 0048), the mesh's own secret delivery is asymmetric and belongs to the host, and
// nothing else called it. The primitive left with the provisioner that was its only caller. // nothing else called it. The primitive left with the provisioner that was its only caller.
// --- semver --- // --- semver ---
+2 -2
View File
@@ -2,9 +2,9 @@
// minio and umami: read the contributions the mesh delivered, bring each consumer's resource into // minio and umami: read the contributions the mesh delivered, bring each consumer's resource into
// being through the provider's adapter under the login and password the mesh minted, and withdraw // being through the provider's adapter under the login and password the mesh minted, and withdraw
// what the mesh no longer asks for. A module writes ONLY the adapter — the per-service half — which // what the mesh no longer asks for. 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). // is why this lives in the sdk and the adapter lives in the module (novox/hq ADR 0039).
// //
// **A provider creates the credential the mesh minted, and seals nothing (novox/hq ADR 0053).** The // **A provider creates the credential the mesh minted, and seals nothing (novox/hq ADR 0048).** The
// control plane mints one password per consumer and seals it to this node; the host unseals it into // control plane mints one password per consumer and seals it to this node; the host unseals it into
// the file the contribution names. The provider does not generate a password, does not seal one, and // the file the contribution names. The provider does not generate a password, does not seal one, and
// does not hand one back — the consumer already receives its copy through the mesh's own channel. // does not hand one back — the consumer already receives its copy through the mesh's own channel.
+3 -3
View File
@@ -1,7 +1,7 @@
// The tool-serving harness. A module declares its tools through registerModuleTools; a runtime // The tool-serving harness. A module declares its tools through registerModuleTools; a runtime
// (the mesh's per-node tool host) collects the registrations and serves them through the command // (the mesh's per-node tool host) collects the registrations and serves them through the command
// surface. HOW a tool is declared and served is settled and lives here; the tools themselves, and // surface. HOW a tool is declared and served is settled and lives here; the tools themselves, and
// the API client they call, live in the module (novox/hq ADR 0044). // the API client they call, live in the module (novox/hq ADR 0039).
import type { ToolDefinition } from "../contracts/index.js"; import type { ToolDefinition } from "../contracts/index.js";
import type { Broker } from "../messaging/index.js"; import type { Broker } from "../messaging/index.js";
@@ -66,7 +66,7 @@ export function listTools(env: NodeJS.ProcessEnv = process.env): { module: strin
* other — two tools answering one name is a fault, not a race to resolve. * other — two tools answering one name is a fault, not a race to resolve.
*/ */
export async function serveTools(broker: Broker, env: NodeJS.ProcessEnv = process.env): Promise<() => void> { export async function serveTools(broker: Broker, env: NodeJS.ProcessEnv = process.env): Promise<() => void> {
// Each tool is served on its own key, namespaced by its module (novox/hq ADR 0052): a caller // Each tool is served on its own key, namespaced by its module (novox/hq ADR 0047): a caller
// invokes `<module>.<tool>`, only the module that serves it answers, and the module's account is // invokes `<module>.<tool>`, only the module that serves it answers, and the module's account is
// scoped to serve.<module>.* — so one module cannot answer another's calls. The module is the // scoped to serve.<module>.* — so one module cannot answer another's calls. The module is the
// namespace, so a tool name need only be unique within its module, not across the whole mesh. // namespace, so a tool name need only be unique within its module, not across the whole mesh.
@@ -90,7 +90,7 @@ export async function serveTools(broker: Broker, env: NodeJS.ProcessEnv = proces
}; };
} }
/** The broker key a tool is served on and invoked by — the module namespaces the tool (ADR 0052). */ /** The broker key a tool is served on and invoked by — the module namespaces the tool (ADR 0047). */
export function toolKey(module: string, tool: string): string { export function toolKey(module: string, tool: string): string {
return `${module}.${tool}`; return `${module}.${tool}`;
} }
+3 -3
View File
@@ -107,7 +107,7 @@ test("modules can SERVE: a real async tool, loaded and invoked over the broker",
const stop = await serveTools(broker, {}); const stop = await serveTools(broker, {});
// Invoke it the way a caller (mesh-control's command API) would — over the broker, by module and // Invoke it the way a caller (mesh-control's command API) would — over the broker, by module and
// tool, each served on its own key `demo.create_site` (novox/hq ADR 0052). // tool, each served on its own key `demo.create_site` (novox/hq ADR 0047).
const result = (await invokeTool(broker, "demo", "create_site", { domain: "my-app" })) as { snippet: string }; const result = (await invokeTool(broker, "demo", "create_site", { domain: "my-app" })) as { snippet: string };
assert.match(result.snippet, /data-website-id="site-123"/); assert.match(result.snippet, /data-website-id="site-123"/);
@@ -120,7 +120,7 @@ test("modules can SERVE: a real async tool, loaded and invoked over the broker",
}); });
test("serving refuses one module exposing two tools of the same name", async () => { test("serving refuses one module exposing two tools of the same name", async () => {
// Two *modules* may share a tool name — each is served on its own `module.tool` key (ADR 0052). // Two *modules* may share a tool name — each is served on its own `module.tool` key (ADR 0047).
// What is refused is one module exposing the same name twice, where the key would collide. // What is refused is one module exposing the same name twice, where the key would collide.
resetTools(); resetTools();
registerModuleTools("a", () => [ registerModuleTools("a", () => [
@@ -197,7 +197,7 @@ test("events: a module emits, a listener and the audit sink (#) both receive it,
assert.deepEqual(heard.map((e) => e.type), ["module.umami.site.created"]); assert.deepEqual(heard.map((e) => e.type), ["module.umami.site.created"]);
assert.deepEqual(audited.map((e) => e.type), ["module.umami.site.created", "module.plex.play.started"]); assert.deepEqual(audited.map((e) => e.type), ["module.umami.site.created", "module.plex.play.started"]);
// The metadata an audit trail needs is present — read back from the ADR 0047 headers, not the body. // The metadata an audit trail needs is present — read back from the ADR 0042 headers, not the body.
const e = heard[0]; const e = heard[0];
assert.equal(e.source, "umami"); assert.equal(e.source, "umami");
assert.equal(e.node, "anchor"); assert.equal(e.node, "anchor");