ruvnet/ruflo · error · TypeError

Federation canonicalization rejects cyclic values

Error message

Federation canonicalization rejects cyclic values

What it means

Cycle detection during JCS canonicalization: an ancestors set of in-progress objects catches circular references before recursion, throwing instead of overflowing the stack. JSON has no reference syntax, so a cyclic payload cannot be represented canonically and must be rejected — both sides could never agree on serialized bytes.

Source

Thrown at v3/@claude-flow/plugin-agent-federation/src/application/inbound-dispatcher.ts:171

      }
      if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
        throw new TypeError('Federation canonicalization rejects unsafe integers');
      }
      return JSON.stringify(value);
    case 'bigint':
    case 'function':
    case 'symbol':
    case 'undefined':
      throw new TypeError(`Federation canonicalization rejects ${typeof value}`);
    case 'object':
      break;
    default:
      throw new TypeError(`Federation canonicalization rejects ${typeof value}`);
  }

  const object = value as object;
  if (ancestors.has(object)) {
    throw new TypeError('Federation canonicalization rejects cyclic values');
  }
  ancestors.add(object);

  try {
    if (Array.isArray(value)) {
      const items: string[] = [];
      for (let index = 0; index < value.length; index += 1) {
        if (!Object.prototype.hasOwnProperty.call(value, index)) {
          throw new TypeError('Federation canonicalization rejects sparse arrays');
        }
        items.push(canonicalizeJcsValue(value[index], ancestors));
      }
      return `[${items.join(',')}]`;
    }

    const prototype = Object.getPrototypeOf(value);
    if (prototype !== Object.prototype && prototype !== null) {
      throw new TypeError('Federation canonicalization accepts only plain objects');

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Restructure the payload as a tree: replace back-references with an id the receiver resolves
  2. Serialize explicitly with a mapper that emits plain data (no parent pointers)
  3. Run a JSON.stringify pre-flight — it throws on cycles before you reach the canonicalizer, with a clearer origin

Example fix

// before
msg.meta.parent.child = msg.meta; // cycle
// after
msg.meta.parent.childId = msg.meta.id; // reference by id
Defensive patterns

Strategy: validation

Validate before calling

// cheap pre-flight: JSON.stringify throws on cycles before canonicalization
try {
  JSON.stringify(envelope.metadata);
} catch (e) {
  throw new Error('refusing to sign cyclic metadata', { cause: e });
}

Type guard

function isAcyclic(value: unknown, ancestors: WeakSet<object> = new WeakSet()): boolean {
  if (typeof value !== 'object' || value === null) return true;
  if (ancestors.has(value)) return false;
  ancestors.add(value);
  try {
    return Object.values(value).every(v => isAcyclic(v, ancestors));
  } finally {
    ancestors.delete(value);
  }
}

Prevention

When it happens

Trigger: Message metadata with parent-child cycles (a.child = b; b.parent = a); self-referential objects (obj.self = obj); shared back-references used to express DAGs inside signed payloads.

Common situations: Tree structures wired with parent pointers for convenience; ORM/graph model objects placed directly into envelopes; caches attaching bookkeeping back-links to cached nodes.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/5f91553a3ff7840c. Report an issue: GitHub.