Files
mesh-catalog/modules/mosquitto/tools/index.ts
T
jochen 13b7562c47
mesh/merge-gate pass: builds docker, keycloak, minio, mosquitto → ace, g14, novox, shanks; no bus step; 2 wait(s) for a person; every machine composes with…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Keep secrets off command lines the runtime records (hq issue 282)
mosquitto passed the broker's admin password to mosquitto_ctrl as -P on
every docker exec, and the container runtime keeps every exec's command
line in its event stream, where docker_events returned it. The admin
credentials now reach mosquitto_ctrl as a 0600 options file fed on
stdin, client passwords at its own prompt, and an argv carrying a secret
is refused before it runs. The admin secret says it is taken at start:
the bootstrap re-runs when the mesh replaces it and re-keys the broker
online from the value it last applied, so it can be rotated.

docker_events redacts what an exec's command line carried, and
docker_secrets_in_events names such secrets by name. keycloak's repair
hands kcadm its passwords through KC_CLI_PASSWORD; minio gives mc its
root alias through MC_HOST_mesh.
2026-10-07 01:37:21 +02:00

57 lines
2.3 KiB
TypeScript

// mosquitto's tools — mosquitto's own code (novox/hq ADR 0039), importing mosquitto's own admin
// client. They return structured data; the mesh serves them through the sdk's tool harness.
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { MosquittoClient, passwordOnArgv } from "../client.js";
export function getMosquittoTools(mosquitto: MosquittoClient): ToolDefinition[] {
return [
{
name: "mqtt_list_clients",
description: "List the Dynamic Security clients registered on the mosquitto broker.",
input: {},
run: async () => ({ clients: await mosquitto.listClients() }),
},
{
name: "mqtt_get_client",
description: "Show one Dynamic Security client — its roles and enabled state.",
input: { username: { type: "string", description: "the client's username" } },
run: async (args) => {
const username = String(args.username ?? "");
if (!username) throw new Error("mqtt_get_client: username is required");
return { username, detail: await mosquitto.ctl("getClient", username) };
},
},
{
name: "mqtt_ctrl",
description:
"Run an arbitrary 'mosquitto_ctrl dynsec' subcommand, e.g. 'listRoles', 'getRole myrole'. Admin surface.",
input: { command: { type: "string", description: "the dynsec subcommand and its arguments, space-separated" } },
run: async (args) => {
const parts = tokenize(String(args.command ?? ""));
if (parts.length === 0) throw new Error("mqtt_ctrl: empty command");
const why = passwordOnArgv(parts);
if (why) throw new Error(`mqtt_ctrl: ${why}`);
const output = await mosquitto.ctl(...parts);
return { command: parts.join(" "), output };
},
},
];
}
/** Split a command line into arguments, honouring double-quoted spans. */
function tokenize(command: string): string[] {
const matches = command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [];
return matches.map((p) => p.replace(/^"|"$/g, ""));
}
// The tools exist only when the broker can be reached from the environment; without it, mosquitto
// contributes none rather than failing the whole tool runtime.
registerModuleTools("mosquitto", (env) => {
try {
return getMosquittoTools(MosquittoClient.fromEnv(env));
} catch {
return [];
}
});