mesh/merge-gate pass: builds mesh-vault → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery failed: its walk failed: a gate on a first machine (what it carried put back), a build, a machine
A condition told the operator to type the node-engine's binary at the machine, and nothing held the catalogue's texts to ADR 0272. The words check gains retired-lines — the controller's and the node-engine's commands, copied from hq's words.py --list lines, which compares it — and refuses one in any string a module's tools can show or any manifest description. The vault's and the route adapter's texts now say the nox line.
132 lines
5.9 KiB
TypeScript
132 lines
5.9 KiB
TypeScript
// 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 " +
|
|
"`nox secret recover --from-export` on the control-node. 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 [];
|
|
}
|
|
});
|