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,48 @@
|
||||
// The broker and event framework a module's code uses to talk to the mesh. The transport is the
|
||||
// mesh's one broker (novox/hq ADR 0001); this is the typed surface over it. The concrete broker
|
||||
// binding is provided by the runtime that hosts a module's code — the sdk defines the contract so
|
||||
// module code, the tool runtime and provisioners all speak it the same way.
|
||||
|
||||
import type { Envelope } from "../contracts/index.js";
|
||||
|
||||
export type { Envelope };
|
||||
|
||||
/** A request/reply call and a publish/subscribe surface over the mesh broker. */
|
||||
export interface Broker {
|
||||
/** Ask one question and await one answer — the shape mesh-control's command API is reached by. */
|
||||
request<Req, Res>(key: string, body: Req): Promise<Res>;
|
||||
/** Emit an event onto the mesh. */
|
||||
publish<T>(env: Envelope<T>): Promise<void>;
|
||||
/** React to events matching a routing-key pattern. Returns an unsubscribe. */
|
||||
subscribe<T>(pattern: string, handler: (env: Envelope<T>) => Promise<void>): Promise<() => void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* How a module obtains its broker. The hosting runtime sets this once; module code calls broker()
|
||||
* without knowing the concrete binding. Keeping the binding out of the sdk is deliberate — the sdk
|
||||
* carries the contract, not a specific AMQP client build.
|
||||
*/
|
||||
let binding: (() => Broker) | undefined;
|
||||
|
||||
export function useBroker(factory: () => Broker): void {
|
||||
binding = factory;
|
||||
}
|
||||
|
||||
export function broker(): Broker {
|
||||
if (!binding) {
|
||||
throw new Error(
|
||||
"no broker is bound — the tool runtime or provisioner host must call useBroker() before " +
|
||||
"module code reaches the mesh",
|
||||
);
|
||||
}
|
||||
return binding();
|
||||
}
|
||||
|
||||
/** A convenience for the common case: consume one event stream until stopped. */
|
||||
export async function consume<T>(
|
||||
pattern: string,
|
||||
handler: (env: Envelope<T>) => Promise<void>,
|
||||
): Promise<() => void> {
|
||||
return broker().subscribe<T>(pattern, handler);
|
||||
}
|
||||
Reference in New Issue
Block a user