{"record":{"id":"3ddc0c30ccc677fa","repo":"ruvnet/ruflo","slug":"nodeid-input-nodeid-is-already-pinned-to-a-different","errorCode":null,"errorMessage":"nodeId ${input.nodeId} is already pinned to a different publicKey; remove it first","messagePattern":"nodeId (.+?) is already pinned to a different publicKey; remove it first","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/agentbbs-federation.ts","lineNumber":276,"sourceCode":"}\n\nexport function addPeer(\n  basePath: string,\n  input: { nodeId: string; url: string; publicKey: string; label?: string },\n): FederationPeer {\n  if (!NODE_ID_RE.test(input.nodeId ?? '')) throw new Error('nodeId must be 16 lowercase hex chars');\n  if (!HEX64_RE.test(input.publicKey ?? '')) throw new Error('publicKey must be 64 lowercase hex chars');\n  const url = validatePeerUrl(String(input.url));\n\n  const peers = readPeers(basePath);\n  if (peers.length >= MAX_PEERS) throw new Error(`peer registry is full (${MAX_PEERS})`);\n\n  const existing = peers.find(p => p.nodeId === input.nodeId);\n  if (existing) {\n    // Re-pinning a different key for a known nodeId is how a key-substitution\n    // attack would present. Require an explicit remove first.\n    if (existing.publicKey !== input.publicKey) {\n      throw new Error(`nodeId ${input.nodeId} is already pinned to a different publicKey; remove it first`);\n    }\n    existing.url = url;\n    if (input.label) existing.label = input.label;\n    writePeers(basePath, peers);\n    return existing;\n  }\n\n  const peer: FederationPeer = {\n    nodeId: input.nodeId,\n    url,\n    publicKey: input.publicKey,\n    label: input.label,\n    addedAt: new Date().toISOString(),\n    lastSeq: {},\n  };\n  peers.push(peer);\n  writePeers(basePath, peers);\n  return peer;","sourceCodeStart":258,"sourceCodeEnd":294,"githubUrl":"https://github.com/ruvnet/ruflo/blob/2602b642d92234c710ffbe96bfb33007d481ceab/v3/@claude-flow/cli/src/mcp-tools/agentbbs-federation.ts#L258-L294","documentation":"addPeer() pins each nodeId to the first publicKey it sees; changing that key later is exactly what a key-substitution (MITM) attack looks like, so the library refuses to overwrite it. To re-key a node you must explicitly remove the peer first and then add it with the new key. Updating url/label for a matching key is still allowed.","triggerScenarios":"Calling addPeer() with a nodeId already present in the registry whose stored publicKey differs from input.publicKey — e.g. the node regenerated its identity key, or two different machines claim the same nodeId.","commonSituations":"Operator rotated a node's keypair and forgot to remove the old peer entry first; nodeId collision after cloning a node image; a peer presenting a forged key for a known nodeId (the attack this check exists to catch).","solutions":["Intentionally rotate: removePeer(base, nodeId) then addPeer() with the new publicKey.","If the key change is unexpected, treat it as a security signal — verify out-of-band with the peer owner before replacing the pinned key.","Ensure nodeId uniqueness across your fleet so cloned nodes don't collide; regenerate identity on cloned machines.","Keep the pinned key in your deployment config in sync with the node's actual key to avoid accidental mismatch."],"exampleFix":"// before\naddPeer(base, { nodeId, url, publicKey: newKey }); // throws: pinned to different key\n// after\nremovePeer(base, nodeId); // explicit, audited key rotation\naddPeer(base, { nodeId, url, publicKey: newKey });","handlingStrategy":"try-catch","validationCode":"const peers = readPeers(basePath);\nconst existing = peers.find(p => p.nodeId === input.nodeId);\nif (existing && existing.publicKey !== input.publicKey) throw new Error('key rotation required: remove peer first');","typeGuard":null,"tryCatchPattern":"try {\n  addPeer(basePath, input);\n} catch (e) {\n  if (/pinned to a different publicKey/.test(e.message)) {\n    // require explicit, audited rotation — do not auto-replace in production\n    removePeer(basePath, input.nodeId);\n    addPeer(basePath, input);\n  } else throw e;\n}","preventionTips":["Treat any key mismatch as a security incident until verified out-of-band","Run an explicit key-rotation runbook: remove then re-add with the new key","Regenerate node identity keys on cloned machines to avoid nodeId collisions","Keep pinned keys in version-controlled config synced with node keys"],"tags":["security","key-pinning","conflict","federation","tofu"],"backgroundTag":"conflicting-config-options","analyzedSha":"2602b642d92234c710ffbe96bfb33007d481ceab","analyzedAt":"2026-09-15T22:58:14.805Z","contentChangedAt":"2026-09-15T22:58:14.805Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}