25 Commits
Author SHA1 Message Date
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