ruvnet/ruflo · error

nodeId is already pinned to a different publicKey; remove…

Error message

nodeId ${input.nodeId} is already pinned to a different publicKey; remove it first

What it means

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.

Solutions

  1. Intentionally rotate: removePeer(base, nodeId) then addPeer() with the new publicKey.
  2. 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.
  3. Ensure nodeId uniqueness across your fleet so cloned nodes don't collide; regenerate identity on cloned machines.
  4. Keep the pinned key in your deployment config in sync with the node's actual key to avoid accidental mismatch.

Example fix

// before
addPeer(base, { nodeId, url, publicKey: newKey }); // throws: pinned to different key
// after
removePeer(base, nodeId); // explicit, audited key rotation
addPeer(base, { nodeId, url, publicKey: newKey });
Defensive patterns

Strategy: try-catch

Validate before calling

const peers = readPeers(basePath);
const existing = peers.find(p => p.nodeId === input.nodeId);
if (existing && existing.publicKey !== input.publicKey) throw new Error('key rotation required: remove peer first');

Try / catch

try {
  addPeer(basePath, input);
} catch (e) {
  if (/pinned to a different publicKey/.test(e.message)) {
    // require explicit, audited rotation — do not auto-replace in production
    removePeer(basePath, input.nodeId);
    addPeer(basePath, input);
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-09-15). Data as JSON: /api/errors/3ddc0c30ccc677fa. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/mcp-tools/agentbbs-federation.ts:276

}

export function addPeer(
  basePath: string,
  input: { nodeId: string; url: string; publicKey: string; label?: string },
): FederationPeer {
  if (!NODE_ID_RE.test(input.nodeId ?? '')) throw new Error('nodeId must be 16 lowercase hex chars');
  if (!HEX64_RE.test(input.publicKey ?? '')) throw new Error('publicKey must be 64 lowercase hex chars');
  const url = validatePeerUrl(String(input.url));

  const peers = readPeers(basePath);
  if (peers.length >= MAX_PEERS) throw new Error(`peer registry is full (${MAX_PEERS})`);

  const existing = peers.find(p => p.nodeId === input.nodeId);
  if (existing) {
    // Re-pinning a different key for a known nodeId is how a key-substitution
    // attack would present. Require an explicit remove first.
    if (existing.publicKey !== input.publicKey) {
      throw new Error(`nodeId ${input.nodeId} is already pinned to a different publicKey; remove it first`);
    }
    existing.url = url;
    if (input.label) existing.label = input.label;
    writePeers(basePath, peers);
    return existing;
  }

  const peer: FederationPeer = {
    nodeId: input.nodeId,
    url,
    publicKey: input.publicKey,
    label: input.label,
    addedAt: new Date().toISOString(),
    lastSeq: {},
  };
  peers.push(peer);
  writePeers(basePath, peers);
  return peer;

View on GitHub (pinned to 2602b642d9)