The runtime names the caller it took from the subject a call arrived on in the
call's _meta; stdio.CallerOf(args) hands it to the tool. Never read from the
arguments, and Unproven when the runtime named none, so a bundle on an older
runtime still says, explicitly, that its caller was not proven.
mesh/delivery covered after its stop: a later walk that contains it was delivered: plan-1791659640519725407 (novox/mesh-sdk at 19cfe0ef into main, done, …
A secret given at the terminal (secret accept … --from -) has no way through mesh-cli today: the controller
runs every line with no standard input. The request now carries it, refused over 64 KiB by mesh-cli and by
the reader alike, and MaxRequest grows by what base64 makes of that bound.
mesh/delivery covered after its stop: a later walk that contains it was delivered: plan-1791659640519725407 (novox/mesh-sdk at 19cfe0ef into main, done, …
For skipped the expiry check when the warrant carried no time, and an ask with a zero expiry was
answered at any time. Neither can be shown to be in time, so neither is acted on.
The review of 2026-10-09: a digest over Go's JSON of a struct changes when a tag is renamed or a field
added, and another language cannot reproduce it. An act is now a flat map of named strings, and both
digests are over length-prefixed values in a fixed order, versioned; a test fails when a field is added
to Ask or Option without being added to the digest.
A warrant named the ask by id and the option by id, and the asker acted on whatever its record held
for them. Now the ask carries, for every option above acknowledge, the digest of exactly what it
performs (Option.Binds, ActDigest), the router puts the digest of the ask it took in the warrant
(Warrant.AskDigest), For refuses a warrant whose digest is not that of the ask the asker keeps, and
Option.Performs refuses an act the option did not bind. A warrant then authorises one act, as the
person was shown it, once.
An unreadable, unparsable or unrecognised contributions file was read as "no consumer asks", and the
reconcile pass then removed every consumer it had applied. On 2026-10-04 one such read made the
postgres provider drop every database on the control node. Such a pass now applies and removes
nothing and says why, once; only a file read with a given list withdraws a consumer, and every
removal is logged before it is made.
The harness says 'secret not readable yet' when it cannot open a consumer's
sealed credential, which is true and ordinary on the first pass. It said it
4330 times in three hours on the control machine while two consumers were
never provisioned, because a permission the host will never grant reads
exactly like a file that has not arrived.
After about a minute it says so plainly instead, once and then rarely, and
names what to look at: who owns the file and who this process runs as. The
ownership itself is the controller's to fix.
0.1.9.
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.
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.
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).
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