Commit Graph
50 Commits
Author SHA1 Message Date
jochen a75add0d59 Module state is hq ADR 0201 after all: the derived-value record moved to 0202 on hq main 2026-10-04 11:02:45 +02:00
mesh-admin dd3ec8da73 Merge pull request 'Module state is hq ADR 0202 (0201 landed first for a provider's derivations)' (#19) from fix/adr-0202-module-state into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 09:01:24 +00:00
mesh-admin f7a3ffe1bf Merge pull request '0.1.8: 0.1.7 was published twice, and the first one won' (#20) from fix/sdk-0-1-8-after-a-version-collision into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 02:05:20 +00:00
jschoubben b55b326201 0.1.8: 0.1.7 was published twice, and the first one won
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.
2026-10-04 04:04:58 +02:00
mesh-admin 9f875b3d5c Merge pull request 'A provisioner is told what the mesh derived for each consumer (hq ADR 0201, issue 124)' (#10) from feat/a-provider-declares-what-it-derives into main
mesh/delivery delivered
2026-10-04 01:47:42 +00:00
jochen d937c7bc2f Module state is hq ADR 0202: 0201 landed first for a provider's derivations 2026-10-04 03:44:56 +02:00
mesh-admin 0677cebb9b Merge pull request 'A module's state through the runtime, in TypeScript and Go (hq ADR 0201)' (#18) from feat/module-state-on-the-bus into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 01:43:35 +00:00
jochen 096c5562a4 A module's state through the runtime, in TypeScript and Go (novox/hq ADR 0201)
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.
2026-10-04 02:47:31 +02:00
jschoubben f1485257a8 A provisioner is told what the mesh derived for each consumer (hq ADR 0201)
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.
2026-10-04 02:44:50 +02:00
mesh-admin 0b018562e3 Merge pull request 'The Go SDK subscribes and asks through the runtime (hq ADR 0198)' (#17) from feat/go-sdk-subscribes-and-asks into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 21:20:04 +00:00
jochen 81ca305ddd The Go SDK subscribes and asks through the runtime (hq ADR 0198)
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.
2026-10-03 23:19:54 +02:00
mesh-admin 7b728a8f9f Merge pull request 'A launched bundle subscribes and asks through its runtime (hq ADR 0198)' (#15) from feat/0198-the-runtime-is-the-bus into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 21:16:34 +00:00
mesh-admin 7f1bbdd234 Merge pull request 'The Go SDK serves as the module the runtime names, answers calls concurrently, and emits through the runtime (hq ADR 0193)' (#16) from feat/go-sdk-serves-like-the-typescript into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 20:24:29 +00:00
jochen b95448be60 The Go SDK serves as the module the runtime names, answers calls concurrently, and emits through the runtime (hq ADR 0193)
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.
2026-10-03 22:24:16 +02:00
jochen 1f936dc4a6 A launched bundle subscribes and asks through its runtime, and its code may do so while being imported (hq ADR 0198)
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.
2026-10-03 22:23:16 +02:00
mesh-admin 16370de2be Merge pull request 'A launched tool emits through its runtime, and calls run beside the stdio loop (hq ADR 0193)' (#14) from feat/0193-a-launched-bundle-emits-through-the-runtime into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 19:12:26 +00:00
jochen fdd5c58a87 A launched tool emits through its runtime, and calls run beside the stdio loop (hq ADR 0193)
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.
2026-10-03 21:12:14 +02:00
mesh-admin 9c2399d595 Merge pull request 'A launched bundle serves under the module the runtime names (hq ADR 0193)' (#13) from feat/0193-served-as-its-module into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 19:05:47 +00:00
jochen dc59ec0e1c A launched bundle serves under the module the runtime names (hq ADR 0193)
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.
2026-10-03 21:05:38 +02:00
mesh-admin 66a3e223f1 Merge pull request 'collectToolsEach: each registration is given its own environment (hq ADR 0192)' (#12) from feat/0192-each-registration-its-own-env into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 13:32:34 +00:00
jochen e63dfc68cd collectToolsEach: each registration is given its own environment (hq ADR 0192)
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.
2026-10-03 15:28:48 +02:00
mesh-admin 67359131e9 Merge pull request 'Cite ADR 0188, not 0187: the record was renumbered before it merged' (#11) from fix/cite-adr-0188 into main
mesh/delivery delivered
2026-10-02 19:29:32 +00:00
jochen 6a1f3fb51f Cite ADR 0188, not 0187: the record was renumbered before it merged (0187 is the dead-tracker record) 2026-10-02 21:29:07 +02:00
mesh-admin e08a10f55f Merge pull request 'A tools bundle speaks MCP over stdio: the loop in TypeScript, and skeleton SDKs for Python, Go, Rust and C (hq ADR 0187)' (#9) from feat/the-operators-machine into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 19:27:47 +00:00
jochen 78817c949e A tools bundle speaks MCP over stdio: the loop in TypeScript, and skeleton SDKs for Python, Go, Rust and C (hq ADR 0187)
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.
2026-10-02 20:34:19 +02:00
jschoubben 163f85ac35 Merge pull request 'The sdk names no transport, and fixtures say what two implementations may not disagree about' (#8) from feat/nats-genesis into main 2026-09-27 17:08:39 +00:00
jschoubben 8f5b786a6b Conformance fixtures: what two implementations may not disagree about
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.
2026-09-26 23:40:59 +02:00
jschoubben debd703d4e The sdk names no transport
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).
2026-09-26 23:33:34 +02:00
jschoubben ffe49b5928 Merge pull request 'Provisioner asks the backend, not memory, whether a consumer is still there (hq issue 120)' (#7) from fix/120-a-provisioner-checks-what-is-there into main 2026-09-25 23:30:23 +00:00
jochen 3192491df6 Brake counts only successful re-applies; only a timeout ends a pass
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.
2026-09-26 01:30:13 +02:00
jochen 3d0165559a Bound holds: a timeout, a brake, and no password in the log
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.
2026-09-26 01:24:30 +02:00
jochen 7976510028 Provisioner asks the backend, not memory, whether a consumer is still there
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.
2026-09-26 00:53:12 +02:00
jschoubben 05ed13b041 Merge pull request 'The SDK is a published package; mesh-controller/foundation rename' (#6) from feat/a-bed-that-hands-over-nothing into main 2026-09-16 23:23:49 +02:00
jschoubben f062235c5e Rename mesh-control -> mesh-controller, substrate -> foundation
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
2026-09-16 18:40:40 +02:00
jschoubben 286cf0bba2 The SDK is a module the mesh builds and publishes
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
2026-09-16 10:27:26 +02:00
jschoubben 49c825f5ba Remove the dead contract types that contradicted the live wire
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
2026-09-16 00:06:37 +02:00
jschoubben 77ebcb3904 Merge pull request 'Publish the compiled output, which is the whole package' (#5) from feat/publish-what-it-builds into main 2026-09-14 11:08:50 +02:00
jschoubben 9213336e9d Publish the compiled output, which is the whole package
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.
2026-09-14 11:05:34 +02:00
jschoubben 160f147a6c Merge pull request 'Resync hq ADR references after the record reconciliation' (#4) from feat/adr-ref-resync into main 2026-09-13 11:15:49 +02:00
jschoubben a1ed33b2b9 The sdk compiles itself when installed from git
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.
2026-09-13 02:38:32 +02:00
jschoubben 6e14896366 Merge pull request 'Resync hq ADR references (0044-0054 -> 0039-0049)' (#3) from feat/adr-ref-resync into main 2026-09-05 12:44:38 +02:00
jschoubben 9b7817c8ca Resync hq ADR references 0044-0054 -> 0039-0049 after the hq record reconciliation 2026-09-05 12:44:17 +02:00
jschoubben 4973b69786 Merge pull request 'SDK: per-key tool serving (ADR 0052) and the provider contract (ADR 0053)' (#2) from events/tool-per-key into main 2026-09-05 03:02:41 +02:00
jschoubben 285c1f1653 test: provisioner test for the ADR 0053 contract; fix stale tool tests
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
2026-09-05 00:33:55 +02:00
jschoubben 436f12edce provisioner: a provider consumes the mesh's credential, seals nothing (ADR 0053)
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
2026-09-05 00:27:24 +02:00
jschoubben 6ef6c761b2 tools: serve each tool on its own module-namespaced key (ADR 0052)
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.
2026-09-04 21:56:03 +02:00
jschoubben 20f7bd2a7b events: metadata rides as headers, not in the body (ADR 0047)
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
2026-09-04 00:25:55 +02:00
jschoubben f335bfb9e7 events: modules log activity to the broker, any module reacts
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
2026-09-03 23:28:38 +02:00
jschoubben 23bb53682b tools serve: the dispatch harness, proven end to end
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
2026-09-03 23:12:38 +02:00
jschoubben a19a2f5cf0 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
2026-09-03 23:01:59 +02:00