ruvnet/ruflo · error
node-identity.json is malformed
Error message
node-identity.json is malformed
What it means
getNodeIdentity loads (or creates) the node identity file node-identity.json under basePath. If the file exists but its nodeId is not 16 lowercase hex chars or its publicKey is not 64 hex chars, the library throws this error rather than trusting a corrupted identity. It indicates the persisted identity file is corrupt, hand-edited, or was written by an incompatible version.
Solutions
- Inspect node-identity.json and fix nodeId to exactly 16 lowercase hex chars ([0-9a-f]{16}) and publicKey to 64 lowercase hex chars
- Delete the malformed node-identity.json and let getNodeIdentity regenerate a fresh keypair (note: this changes the node's identity, so peers referencing the old nodeId/publicKey must be updated)
- Verify the file is valid JSON (no truncation, no concatenation of two objects) and contains only the expected fields
- Restore the file from backup if the original identity must be preserved
Example fix
// before: hand-edited, malformed
{"nodeId":"my-node","publicKey":"abc123"}
// after: valid identity
{"nodeId":"0a1b2c3d4e5f6071","publicKey":"<64 lowercase hex chars>"} Defensive patterns
Strategy: try-catch
Validate before calling
const id = JSON.parse(readFileSync(p, 'utf-8'));
const ok = /^[0-9a-f]{16}$/.test(id?.nodeId ?? '') && /^[0-9a-f]{64}$/.test(id?.publicKey ?? '');
if (!ok) throw new Error('local node-identity.json is malformed; fix or delete it'); Type guard
function isValidNodeIdentity(v: unknown): v is NodeIdentity {
const o = v as NodeIdentity;
return !!o && typeof o.nodeId === 'string' && /^[0-9a-f]{16}$/.test(o.nodeId)
&& typeof o.publicKey === 'string' && /^[0-9a-f]{64}$/.test(o.publicKey);
} Try / catch
let identity;
try {
identity = await getNodeIdentity(basePath);
} catch (e) {
if (e.message === 'node-identity.json is malformed') {
// back up the bad file, then regenerate
renameSync(identityPath(basePath), identityPath(basePath) + '.bad');
identity = await getNodeIdentity(basePath);
} else throw e;
} Prevention
- Never hand-edit node-identity.json; regenerate identities via the API
- Write the identity file atomically (temp file + rename) to avoid truncation on crash
- Back up the file before upgrades or migrations
- Validate with the regexes after any manual copy between nodes
When it happens
Trigger: Calling getNodeIdentity(basePath) when node-identity.json exists at identityPath(basePath) but JSON-parses to an object whose parsed.nodeId fails NODE_ID_RE or whose parsed.publicKey fails HEX64_RE (including undefined/missing fields via the ?? '' fallback).
Common situations: Manual edits to node-identity.json; truncation/partial writes after a crash; copying an identity file from another node and editing fields by hand; older library versions writing a different format; shell-mangled or concatenated file content.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- channel key must be 32 bytes (64 hex)
- actualUsd must be a non-negative finite number
- Agent config must include id, name, and type
- agent must be an object
- anchor task has no query
AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-09-15).
Data as JSON: /api/errors/3b027b2a378e7b42.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-tools/agentbbs-federation.ts:183
function roomLogPath(basePath: string, roomId: string): string {
return join(basePath, `room-${roomId}.jsonl`);
}
/**
* Load or create this host's long-lived Ed25519 identity.
*
* Phase 1 minted an ephemeral key per process, which is fine for local token
* signing but useless across hosts: a peer cannot pin a key that changes on
* every restart. This persists one, 0600, and derives a stable nodeId from the
* public key so identity is verifiable rather than self-asserted.
*/
export async function getNodeIdentity(basePath: string): Promise<NodeIdentity> {
ensureDir(basePath);
const p = identityPath(basePath);
if (existsSync(p)) {
const parsed = JSON.parse(readFileSync(p, 'utf-8')) as NodeIdentity;
if (!NODE_ID_RE.test(parsed.nodeId ?? '') || !HEX64_RE.test(parsed.publicKey ?? '')) {
throw new Error('node-identity.json is malformed');
}
return parsed;
}
const ed = await loadEd25519();
const priv: Uint8Array = ed.utils?.randomPrivateKey ? ed.utils.randomPrivateKey() : new Uint8Array(randomBytes(32));
const pub: Uint8Array = await (ed.getPublicKeyAsync ?? ed.getPublicKey)(priv);
const publicKey = hex(pub);
const identity: NodeIdentity = {
nodeId: createHash('sha256').update(`agentbbs:node:${publicKey}`).digest('hex').slice(0, 16),
publicKey,
privateKey: hex(priv),
createdAt: new Date().toISOString(),
};
writeFileSync(p, JSON.stringify(identity, null, 2) + '\n', { mode: 0o600 });
try { chmodSync(p, 0o600); } catch { /* best effort on filesystems without modes */ }
return identity;
}
View on GitHub (pinned to 2602b642d9)