Convert four hal modules: lidarr, mongodb, mssql, mosquitto

Mirrors the proven catalog patterns field-for-field:
- lidarr  -> the Servarr twin of radarr/sonarr (API v1, artist content); no
  provisioner (it is a consumer app).
- mongodb -> postgres shape: mongodb-database provider, provisioner mints a
  per-consumer db+user (ADR 0053), client shells to mongosh (no npm driver,
  the psql convention).
- mssql   -> postgres shape: mssql-database provider, sqlcmd client.
- mosquitto -> redis shape: mqtt-topic provider via the Dynamic Security
  plugin, deliberately avoiding hal's password_file (that file is nox issue
  011 exactly); provisioner mints a per-consumer MQTT client+role.

All four typecheck (strict, NodeNext) against the built @novox/mesh-sdk, and
their service images are digest-pinned to resolved registry digests. The
mesh-runtime-<mod> images keep the all-zeros placeholder the pipeline pins,
as postgres/redis do, and must bundle each module's CLI (mongosh/sqlcmd/
mosquitto_ctrl) as mesh-runtime-postgres bundles psql.

Not yet lab-verified: each module lists in-code what an integration test must
prove (auth model, provisioner reconcile, mosquitto dynsec bootstrap ordering).

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-05 04:17:07 +02:00
parent bca68c2109
commit c82b3ff706
27 changed files with 1793 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
// mongodb's tools — mongodb's own code (novox/hq ADR 0044), importing mongodb's own client. They
// return structured data; the mesh serves them through the sdk's tool harness. Both call through
// MongoClient.evalJs(), the module's one execution boundary (see client.ts).
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { MongoClient } from "../client.js";
export function getMongoTools(mongo: MongoClient): ToolDefinition[] {
return [
{
name: "mongodb_list_databases",
description: "List the databases on the mongodb server, with their on-disk size.",
input: {},
run: async () => ({ databases: await mongo.listDatabases() }),
},
{
name: "mongodb_query",
description: "Run a read-only find against a collection in a named database and return the matching documents.",
input: {
database: { type: "string", description: "the database to query" },
collection: { type: "string", description: "the collection to read from" },
filter: { type: "object", description: "the MongoDB query filter (defaults to {} — all documents)" },
limit: { type: "number", description: "maximum documents to return (default 100, capped at 1000)" },
},
run: async (args) => {
const database = String(args.database ?? "");
const collection = String(args.collection ?? "");
if (!database) throw new Error("mongodb_query: database is required");
if (!collection) throw new Error("mongodb_query: collection is required");
const filter = isObject(args.filter) ? args.filter : {};
const limit = Number(args.limit ?? 100) || 100;
const documents = await mongo.find(database, collection, filter, limit);
return { database, collection, documents };
},
},
];
}
function isObject(v: unknown): v is Record<string, unknown> {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
// The tools exist only when the server can be reached from the environment; without it, mongodb
// contributes none rather than failing the whole tool runtime.
registerModuleTools("mongodb", (env) => {
try {
return getMongoTools(MongoClient.fromEnv(env));
} catch {
return [];
}
});