Add the mesh-vault module; redis takes its password from it

mesh-vault provides `secret` (novox/hq ADR 0085, design 24). The value is
the pair credential the controller mints — the vault holds no copy, only a
ledger of who holds one, its fingerprint and every rotation, and two tools that
answer by fingerprint and never by value. Rotation is `rotate secret`,
unchanged machinery pointed at a secret with an owner (design 13). Named in the
mesh's own namespace, beside mesh-controller and mesh-catalog, because it is
the mesh's own code rather than wrapped software.

redis is the first consumer: its own password stops being an own-secret nothing
could rotate and becomes a `secret` it requires, read from the same file into
the same hole. The server now restarts on its config, or it would keep the
password it started with through every rotation (playbook 06).
This commit is contained in:
2026-09-21 00:48:13 +02:00
parent 8105ab6141
commit 82e513a360
12 changed files with 635 additions and 6 deletions
+131
View File
@@ -0,0 +1,131 @@
// mesh-vault's tools — vault's own code (novox/hq ADR 0039), served through the sdk's tool harness. They
// return structured data about the secrets this vault provides, and **never a value**: a holder is
// identified by its login and a value by its fingerprint. That is the rule the source mesh's
// secret_locate / secret_verify were built on, after a secret printed into a transcript.
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { Ledger, contributions, deliveredFingerprint, type Held } from "../client.js";
/** The mesh's export of every operator-sealed secret, as the mesh wrote it into the root dir. */
interface KeptExport {
export: number;
"operator-key": string;
fingerprint: string;
kept: { node: string; module: string; name: string; origin: string; sealed: string; key: string; "made-at": string }[];
unrecoverable?: { node: string; module: string; name: string }[];
}
export function getVaultTools(ledger: Ledger, receives: string | undefined, root: string | undefined): ToolDefinition[] {
return [
{
name: "secret_export",
description:
"The mesh's root secrets as this vault keeps them: every secret a module holds for itself, " +
"sealed to the operator's key (novox/hq ADR 0085, amended). Ciphertext — nothing here can " +
"open a line of it; the operator, holding the private key off the mesh, recovers one with " +
"`mesh-controller secret recover --from-export`. Also lists what is NOT recoverable: secrets " +
"made before the mesh had an operator key.",
input: {
sealed: {
type: "boolean",
description: "include the sealed blobs (default true); false lists holders and the key only",
},
},
run: async (args) => {
if (!root) return { available: false, error: "this vault keeps no root secrets (MESH_VAULT_ROOT is not set)" };
let doc: KeptExport;
try {
doc = JSON.parse(readFileSync(join(root, "export.json"), "utf8")) as KeptExport;
} catch (err) {
return { available: false, error: `the mesh has not written an export here yet: ${err}` };
}
const withBlobs = args.sealed !== false;
return {
available: true,
export: doc.export,
"operator-key": doc["operator-key"],
fingerprint: doc.fingerprint,
count: doc.kept.length,
kept: doc.kept.map((k) => (withBlobs ? k : { node: k.node, module: k.module, name: k.name, origin: k.origin, "made-at": k["made-at"] })),
unrecoverable: doc.unrecoverable ?? [],
};
},
},
{
name: "secret_holders",
description:
"Who holds a secret from this vault: each consumer's login, node and module, when it was " +
"granted, how many times it has been rotated and when, and the fingerprint of the current " +
"value. Fingerprints only — the value is never returned.",
input: {},
run: async () => {
const asked = receives ? contributions(receives) : [];
const holders = ledger.list().map((h) => ({
...h,
module: asked.find((c) => c.as === h.as)?.from ?? null,
asked: asked.some((c) => c.as === h.as),
}));
return { holders, count: holders.length };
},
},
{
name: "secret_verify",
description:
"Check one holder's secret without seeing it: the fingerprint the vault recorded against " +
"the fingerprint of the value the mesh currently delivers here, and optionally against a " +
"fingerprint computed on the holder's own machine (sha256 of the file, as `sha256:<hex>`). " +
"Two ends agreeing proves they agree, not that either works — the login itself is the test.",
input: {
as: { type: "string", description: "the holder's login, e.g. anchor-redis" },
fingerprint: {
type: "string",
description: "optional: sha256:<hex> of the value as the holder reads it, computed there — never the value",
},
},
run: async (args) => {
const as = String(args.as ?? "");
const recorded = ledger.get(as);
if (!recorded) return { as, known: false, error: `this vault holds nothing for ${as}` };
const asked = receives ? contributions(receives).find((c) => c.as === as) : undefined;
const delivered = asked ? deliveredFingerprint(asked) : { error: "the mesh does not currently ask this vault to serve that login" };
const given = args.fingerprint ? String(args.fingerprint) : undefined;
return verdict(recorded, delivered, given);
},
},
];
}
function verdict(
recorded: Held,
delivered: { fingerprint: string; length: number } | { error: string },
given: string | undefined,
): Record<string, unknown> {
const deliveredMatches = "fingerprint" in delivered ? delivered.fingerprint === recorded.fingerprint : null;
const givenMatches = given === undefined ? null : given === recorded.fingerprint;
return {
as: recorded.as,
known: true,
recorded: recorded.fingerprint,
rotations: recorded.rotations,
changed: recorded.changed,
delivered: "fingerprint" in delivered ? delivered.fingerprint : null,
deliveredError: "error" in delivered ? delivered.error : null,
deliveredMatchesRecorded: deliveredMatches,
given: given ?? null,
givenMatchesRecorded: givenMatches,
givenIsAnEarlierValue: given === undefined ? null : recorded.history.some((h) => h.fingerprint === given),
ok: deliveredMatches !== false && givenMatches !== false,
};
}
// The tools exist only when the ledger can be reached from the environment; without it, vault
// contributes none rather than failing the whole tool runtime.
registerModuleTools("mesh-vault", (env) => {
try {
return getVaultTools(Ledger.fromEnv(env), env.MESH_RECEIVES, env.MESH_VAULT_ROOT);
} catch {
return [];
}
});