ruvnet/ruflo · error · TypeError

Federation canonicalization rejects non-canonical numbers

Error message

Federation canonicalization rejects non-canonical numbers

What it means

During JCS (RFC 8785-style) canonicalization of a signed federation envelope, numbers must be exactly representable: NaN, ±Infinity, and negative zero (-0) are rejected because their JSON serialization is undefined or ambiguous, and sender/receiver would verify different bytes. The check uses Number.isFinite plus Object.is(value, -0).

Source

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

 * values outside the I-JSON-safe subset instead of silently dropping them.
 * The `signature` field itself is excluded because it is what we verify.
 *
 * Federation messages are wrapped as `AgentMessage{id, type, payload,
 * metadata}` on the wire. The `payload` is the actual FederationEnvelope
 * (per `plugin.ts sendToNode`); we canonicalize the payload + the
 * metadata so the receiver verifies the same bytes the sender signed.
 */
function canonicalizeJcsValue(value: unknown, ancestors: Set<object>): string {
  if (value === null) return 'null';

  switch (typeof value) {
    case 'boolean':
      return value ? 'true' : 'false';
    case 'string':
      return JSON.stringify(value);
    case 'number':
      if (!Number.isFinite(value) || Object.is(value, -0)) {
        throw new TypeError('Federation canonicalization rejects non-canonical numbers');
      }
      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)) {

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Sanitize every numeric before it enters a signed envelope: Number.isFinite(v) && !Object.is(v, -0)
  2. Normalize -0 to 0 (v + 0 or `v === 0 ? 0 : v`)
  3. Convey NaN/Infinity as explicit strings or null if they must be transmitted
  4. Add a pre-sign assertion that walks the payload and rejects non-canonical numbers

Example fix

// before
envelope.metadata.ratio = numerator / denominator; // 0/0 -> NaN
// after
const raw = numerator / denominator;
envelope.metadata.ratio = Number.isFinite(raw) && !Object.is(raw, -0) ? raw : null;
Defensive patterns

Strategy: type-guard

Validate before calling

// sanitize numbers before they enter a signed envelope
function sanitizeNumber(v: number): number | null {
  return Number.isFinite(v) && !Object.is(v, -0) ? v : null;
}

Type guard

function isCanonicalNumber(v: unknown): v is number {
  return (
    typeof v === 'number' &&
    Number.isFinite(v) &&
    !Object.is(v, -0)
  );
}

Prevention

When it happens

Trigger: Envelope metadata carrying NaN or Infinity from a failed computation (0/0, overflow); a -0 produced by Math.round(-0.4), a sign flip, or parsing '-0'; division or averaging code writing raw results into signed metadata.

Common situations: Metrics/latency fields (elapsed = end - start where both are 0) copied into message metadata; normalization math that can yield -0; user-supplied numbers passed through unvalidated.

Related errors


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