Two pull requests bumped 0.1.6 to 0.1.7 within four minutes of each other —
#18 (a module's state through the runtime) and #10 (a provisioner is told
what the mesh derived). Both built and both published @novox/mesh-sdk@0.1.7,
with different contents. The store kept the first.
So the toolchain installs a 0.1.7 that has no `derived`, and every module
compiled in it fails on a field main's source says is there. minio is the
first to hit it; novox's push is refused because of it.
A version is a name for one set of bytes. This takes the next one.
state(name) / stdio.State(name): get, put, delete, keys and watch over the
runtime's mesh/state verbs. A watch's handler is registered under the id it
is asked with, so its current values — which arrive before the watch is
answered — and its changes reach it and no other watch.
Provision gains `derived`; the harness keeps it per applied login so remove()
is told it too, and includes it in the change hash — a provider that renames
what it derives has given the consumer a different resource.
Version 0.1.7. Rebased onto main after the bundles refactor; the record this
cites was written as 0188 and renumbered to 0201, because ADR 0188 on main is
now the bundles record.
Subscribe(pattern, handler) asks the runtime for the module's events and answers each mesh/event
only after the matching handlers returned nil, so the runtime acknowledges it on the bus then and
not before; Ask(key, body) calls a tool as the module. Topic matching is the TypeScript SDK's.
What the TypeScript SDK's stdio loop does, in Go: an empty name serves as MESH_SERVED_MODULE; each
call runs beside the loop so a tool waiting on the runtime does not deadlock it; Emit asks the
runtime (mesh/publish) with the same envelope and headers. README says how to install it and how
releases are tagged.
The stdio channel is the bundle's bus: subscribe asks the runtime (mesh/subscribe), which binds the
module's durable consumer and hands each event over as mesh/event — answered once every matching
handler has taken it, as an error when one failed so the runtime does not acknowledge it; request
asks the runtime (mesh/ask). The session starts as the stdio module loads when the runtime launched
the process, because a handler module subscribes at import, before the launcher says what it
serves; initialize and tools/list are answered once it has. topicMatches as the runtime's. 0.1.6.
A launched bundle holds no bus connection. The stdio loop binds a broker whose publish asks the
runtime (mesh/publish) and resolves when the runtime has published; asking, answering and
subscribing are refused by name. Tool calls run beside the loop, so a tool awaiting the runtime's
answer to its emit does not deadlock the line that carries it. 0.1.5.
serveRegisteredOverStdio lists the registrations of the module named by MESH_SERVED_MODULE by
their own names and every other registration as <seat>.<verb>. Before, the first registration
was taken for the module's own, so a bundle registering its seat first listed the seat's verbs as
its own tools. Contributors are given the process's environment, the bundle's own. 0.1.4.
A node's runtime serves many modules in one process; a module's composed paths and words must
reach its own tools and no other's. The runtime knows which bundle made which registration, so
it says which environment each contributor gets. collectTools(env) is unchanged. 0.1.3.
The node's runtime launches a tools bundle as a process and speaks MCP over stdio to it, so a tool
may be written in any language and the transport stays in the runtime (ADR 0039's refusal, kept).
Each SDK is the loop and the tool type and nothing else: `@novox/mesh-sdk/stdio` (serveStdio, and
serveRegisteredOverStdio for a bundle that already registers the in-process way), python/, go/,
rust/ and c/ beside it, each with an example answering `greet` and the seat verb `node-lamp.on`.
Skeletons, by the operator's direction: the bare minimum one bundle per language needs to be built
and answer, proven here by each example answering tools/list and tools/call on stdio.
Tasks 3.1 and 3.3 of novox/hq ADR 0116. One fixture directory, read by
every implementation's own runner rather than copied into each — a fixture
copied twice is two fixtures, and two fixtures drift.
The README settles what ADR 0074's byte-for-byte can and cannot mean. The
envelope is exact: the subject, the required headers, each name and format,
because those are what two implementations get wrong invisibly. The body is
not: it is the module's payload, and Go sorts a map's keys where JavaScript
keeps insertion order, so demanding identical bytes would commit every
implementation to a canonical JSON encoder to buy a property the mesh never
uses. Said plainly, because read strictly it would have sent somebody
writing one.
Task 3.7 of novox/hq ADR 0116, and the whole of the sdk's diff for the bus
change. Three comments said AMQP where they meant 'message headers' and 'a
broker client'; the code never spoke it, which is why no module is rebuilt
for any of this (ADR 0039).
A lost consumer whose re-apply fails is retried at the next check with
its count unchanged, instead of waiting out a backoff meant for adapters
whose create and holds disagree. A check that fails for one consumer no
longer stops checking the consumers after it; only a timeout does.
A check that hangs no longer stalls every consumer: it times out after
30s and counts as could-not-ask, and the rest of that pass is not asked.
A consumer still not held after being applied again is checked at
doubling intervals up to an hour, and said loudly, so an adapter whose
create and holds disagree costs one re-apply an hour, not one a minute.
The consumer's password is scrubbed from every error the harness logs.
An optional holds() on the adapter is asked for every applied consumer
every minute; false applies it again. A backend that forgets what it was
given while the provisioner runs (hq issue 120) is healed within a
minute instead of failing its consumers in silence. Unable to ask is not
treated as loss. Adapters without holds() behave as before.
One name per thing, per the HQ glossary: the module/container/image/binary/repo
becomes mesh-controller, the seat the-controller, and the store+broker pair the
foundation (embedded base bundles, default template and example lock renamed with
their go:embed directives). No behaviour change — a pure vocabulary rename.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
A package artifact, built on a public base and published to the mesh's package
registry by version, so every module resolves it the ordinary way (hq ADR 0076).
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Grant, Credential and an Interface type lived in contracts, exported and imported
by nothing, describing a grant with fields — resource, consumer — the live wire
does not use. The wire is the contributions file, whose shape (as, secret, node,
at, values) agrees between Go and TypeScript.
These dead types are how ADR 0074 came to claim a drift that inspection does not
find: they read as the contract and were not. A type is only as good as its being
the wire, and one that has drifted from it while still being exported is worse
than no type. Removed.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
It compiles itself on install and then shipped everything except the result.
With no explicit file list npm falls back to .gitignore, which ignores dist —
so every consumer received a package whose every entry point pointed at a
directory that had just been built and then excluded.
The workaround for this lived in mesh-tools, which compiled the dependency by
hand after installing it.
Its entry points all resolve into a compiled directory that is not in source
control, so anything installing it from a clone got a package whose every
export pointed at nothing. npm runs this after a git install; that is what lets
a build with only one repository in front of it depend on this one.
Rewrites the provisioner test to the new contract (a contributions file + an
unsealed secret; the adapter is handed the mesh's login and password, and removal
follows the consumer leaving the file) and drops the seal round-trip test with the
primitive it covered. Also fixes two tool tests left stale by the per-key serving
rework (ADR 0052): invoke by module.tool, and refuse one module's duplicate name
(two modules may now share a name). Suite green: 6 pass.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
runProvisioner now reconciles the mesh's `receives` contributions: for each
consumer it reads the mesh-minted password from the file the host unsealed and calls
the adapter to create the resource under the login the mesh derived. The adapter is
create({as,password,values}) / remove({as}), returning nothing — the consumer
already receives its copy through the mesh's own asymmetric channel. $MESH_SEAL_KEY,
the symmetric seal()/writeSealedCredential path, and the *.grant.json / *.credential
files are gone; the seal()/unseal() primitive had no other caller and was removed.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
serveTools now serves each tool on serve.<module>.<tool> instead of one
tools.invoke that dispatched by name — so a module's account is scoped to
serve.<module>.* and one module cannot answer another's calls. toolKey and
invokeTool are the caller's side. A tool name need only be unique within its
module now, not across the mesh.
emit stamps the ADR 0047 headers — x-event-id, x-source, x-node, x-time,
content-type, and optional x-causation-id / x-schema — and publishes the
body as only the domain payload. on() reconstructs the Event from those
headers. Event gains id (the x-event-id a consumer dedups on) plus the
optional causation/schema. EventHeaders joins the contracts spine.
Supersedes the first cut that carried source/node/time in the body.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
The lighter sibling of provisioning — 1:many and broadcast, no credential,
just the broker's topic routing. A thin, audit-ready surface over the
broker's publish/subscribe:
- emit(type, body): publishes an Event carrying who emitted it (MESH_MODULE),
on which node (MESH_NODE) and when (ISO timestamp) — so a listener can
build a real audit trail.
- on(pattern, handler): react to events by topic pattern. The audit logger
is just on("#", ...).
Tested: a module emits; a targeted listener (module.umami.#) hears only its
events, the audit sink (#) hears every module's, and the metadata audit
needs is present.
The declared side — a manifest's emits/consumes, so the mesh knows the
event graph — and the audit-logger module are the next pieces.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
Add serveTools(broker) — the tool runtime's core: collect every module's
registered tools, index by name (refusing a duplicate name across two
modules rather than silently shadowing), and answer 'tools.invoke'
requests by running the named tool and returning its result. Plus
listTools() for discovery and Broker.handle() (the server side of
request/reply).
Proven by test: a real async, network-calling tool is registered (as a
module does), served over an in-memory broker, and invoked by name — it
reaches its upstream and returns the computed result. So a module's tools
genuinely serve: register -> collect -> serve -> invoke -> real work ->
result. The per-node runtime process that binds the mesh's real broker
and imports the assigned modules is the thin wrapper over this.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
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