A tie went to whichever tool discovery listed first, so docker logs found docker_secrets_in_logs before docker_logs on some runs. A name the query says whole wins, then the shorter name.
96 lines
6.6 KiB
Markdown
96 lines
6.6 KiB
Markdown
# mesh-tools
|
|
|
|
Two modules in one repository (novox/hq ADR 0069), one piece of software:
|
|
|
|
- **`node-tools`** (`node-tools/`) — the node's **tool runtime** as a module (ADR 0175, to-be 38
|
|
WP3): one process per machine the host runs from this bundle, serving every assigned module's tools
|
|
and every held seat's verbs on the bus, and answering MCP on the machine's loopback — the console
|
|
(design 34). The code, its tests and the `mesh` client all live there.
|
|
- **`mesh-tools`** (this directory) — the two images TypeScript bundles are compiled in and a module's
|
|
own *service* may still run in. Built from the same code; no longer how tools reach a node.
|
|
|
|
The runtime:
|
|
|
|
1. connects the mesh bus on the node's credential — a concrete implementation of the sdk's `Broker`
|
|
contract;
|
|
2. reads one membership per module it serves — what the mesh issued that module on this machine
|
|
(ADR 0160): where its tools are answered, which seats it holds — and follows each live;
|
|
3. imports each module's compiled tool entrypoints, each of which registers its tools as it loads,
|
|
guarded: a bundle that throws is named, in the log and in what `tools` answers for its module,
|
|
and the others serve;
|
|
4. serves every module's tools on that module's subjects and every held seat's verbs on the seat's.
|
|
|
|
A bundle that is not plain JavaScript — a Go or Rust binary, a Python script, or a JavaScript file
|
|
marked executable — is **launched** rather than imported (novox/hq ADR 0188): the runtime starts it
|
|
as a child with its own environment and speaks MCP over stdio to it, `tools/list` once and
|
|
`tools/call` per call. A tool it lists as `<seat>.<verb>` is the seat's implementation. A child that
|
|
exits is named in the log and started again on its next call. So a tools bundle may be written in
|
|
any language; the mesh's SDK for each is the stdio loop and nothing more (`node-tools/src/launch.ts`
|
|
is the runtime's side of it).
|
|
|
|
Everything hard — dispatch, collection, duplicate-name safety — is the sdk's. This is the thin
|
|
wrapper that binds the bus and loads the modules. Keeping the bus client here, out of the sdk, is
|
|
deliberate: a bus-client change never rebuilds a module (ADR 0039). The runtime is module-agnostic:
|
|
it knows bundles and subjects, nothing of what any module does. A tool that needs root escalates
|
|
itself — root is the module's concern, not the runtime's.
|
|
|
|
## Running it
|
|
|
|
```
|
|
MESH_BROKER_FILE the node's sealed credential, as the mesh delivered it
|
|
MESH_TOOL_MODULES alpha=/…/alpha/tools/index.js,beta=/…/beta/dist/index.js,…
|
|
the modules to serve and their compiled entrypoints; several entries may
|
|
name one module. A bare path is an entrypoint of the credential's own module
|
|
— the one-module form a per-module container still sets.
|
|
MESH_OPERATOR_ACCOUNT whose machine this is, and MESH_OPERATOR_HOME where their home is; set by
|
|
the mesh when the node has an account, read by tools from their environment
|
|
MESH_BROKER_URL a plain URL instead of the credential, for the bootstrap case
|
|
```
|
|
|
|
`node dist/main.js`. On a node the controller composes the variables and the host supervises the
|
|
process like any other host-side workload (novox/hq to-be 38). As `node-tools` the same process is
|
|
the console: MCP on `127.0.0.1:4270` (or `MESH_CONSOLE_LISTEN`). The container (`Dockerfile`) is how
|
|
a module's own *service* may still be built; it is no longer how tools reach a node.
|
|
|
|
## `mesh` — the tools for whoever is on a machine
|
|
|
|
The same package carries the client (novox/hq design 25 §7, design 34): `mesh tools`, `mesh call
|
|
<module>.<tool> [json]`, `mesh mcp` (an MCP server over stdio for a program a person starts) and
|
|
`mesh serve` (the **console**: MCP over HTTP on a machine's loopback, started by the mesh as the
|
|
`mesh-console` module on the credential in `MESH_BROKER_FILE` — novox/hq ADR 0152). `mesh serve`
|
|
refuses to bind anything but loopback. With `--console <url>`, `tools` and `call` go through a console
|
|
already on the machine and need no credential.
|
|
|
|
The console announces six tools and reaches everything else by address (novox/hq ADR 0195):
|
|
`mesh_overview` (the mesh's seats and machines), `mesh_machine` (one machine's seats and modules),
|
|
`mesh_search` (a tool by words), `mesh_describe` (one tool's arguments), `mesh_call` (call one by
|
|
address) and `mesh_runtimes` (which runtimes answered discovery: per runtime its machine, how long its
|
|
answer took, its size in bytes, how many modules and tools it announced, whether it was shortened to
|
|
fit the bus, when it was last heard — and who was expected and not heard).
|
|
|
|
`mesh_search` also finds a tool by the shell command it replaces (novox/hq ADR 0245): each seat verb and
|
|
module tool says what it replaces in the controller's records (`replaces`, read from the controller's
|
|
`tools` and `modules` answers), so `journalctl -u x`, `systemctl status x` or `docker ps` puts the verb
|
|
for it first, and says the command it matched as `instead_of`. A word also matches through a short table
|
|
of the words agents use for the mesh's (`logs` finds the journal). Best first: a replaced command, then
|
|
words in a name or a replaced command, then words in a description; seats before modules on a tie.
|
|
|
|
Discovery asks the bus (ADR 0197): every runtime answers the NATS services protocol's `$SRV.PING` and
|
|
`$SRV.INFO` with what it serves at that moment, and the console reads where the controller's records
|
|
place each module. The console waits at least 750 ms, and up to 5 s for every runtime that answered
|
|
PING to send what it serves — a large runtime's answer crossing the bus to a broker on another machine
|
|
can arrive well after the rest. A runtime that said it is there and did not say what it serves, or
|
|
that answered earlier and not now, is named: the console never calls a module missing, or on another
|
|
machine, while a runtime that might serve it was not heard. A runtime whose answer outgrows the bus
|
|
announces first-line descriptions, then none, and says so. A module may not name a tool of its own
|
|
`tools`; the runtime refuses it at load.
|
|
|
|
## Verified
|
|
|
|
`npm test` runs against a real NATS server with JetStream (`MESH_TEST_NATS`, see any test's header
|
|
for the one-line `docker run`) and proves the whole path over the wire: the runtime serves a
|
|
registered tool, a separate connection invokes it by name and gets the result, an unknown tool is
|
|
refused, a runtime serves exactly the subjects it is issued and re-serves on a new membership, and
|
|
the node's runtime serves three modules' bundles on one credential — one of them broken, named and
|
|
not fatal — with every seat verb answering where the membership put it.
|