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

  1. Inspect node-identity.json and fix nodeId to exactly 16 lowercase hex chars ([0-9a-f]{16}) and publicKey to 64 lowercase hex chars
  2. 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)
  3. Verify the file is valid JSON (no truncation, no concatenation of two objects) and contains only the expected fields
  4. 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

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.

Related errors


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)