ruvnet/ruflo · error · Error

canonical JSON supports only arrays and plain objects

Error message

canonical JSON supports only arrays and plain objects

What it means

Only arrays and objects whose prototype is Object.prototype or null can be canonicalized; any other object — Date, Map, Set, RegExp, Buffer, or a class instance — throws. instanceof Date would encode as '{}' via JSON.stringify, and subclass prototypes vary across runtimes, so non-plain objects are rejected to keep digests byte-stable.

Source

Thrown at v3/@claude-flow/codex/src/harness/repository-state.ts:137

        !Number.isFinite(item)
        || Object.is(item, -0)
        || (Number.isInteger(item) && !Number.isSafeInteger(item))
      ) {
        throw new Error('canonical JSON requires finite, safe, non-negative-zero numbers');
      }
      return JSON.stringify(item);
    }
    if (item === undefined) throw new Error('canonical JSON does not support undefined');
    if (typeof item !== 'object') {
      throw new Error(`canonical JSON does not support ${typeof item}`);
    }
    if (ancestors.has(item)) throw new Error('canonical JSON does not support cycles');
    ancestors.add(item);
    try {
      if (Array.isArray(item)) return `[${item.map(encode).join(',')}]`;
      const prototype = Object.getPrototypeOf(item);
      if (prototype !== Object.prototype && prototype !== null) {
        throw new Error('canonical JSON supports only arrays and plain objects');
      }
      const entries = Object.entries(item as Record<string, unknown>)
        .sort(([left], [right]) => codeUnitCompare(left, right));
      return `{${entries.map(([key, child]) => {
        assertUnicodeScalarString(key);
        return `${JSON.stringify(key)}:${encode(child)}`;
      }).join(',')}}`;
    } finally {
      ancestors.delete(item);
    }
  };
  return encode(value);
}

function gitBuffer(repoRoot: string, args: readonly string[]): Buffer {
  return execFileSync('git', ['-C', repoRoot, ...args], {
    encoding: 'buffer',
    stdio: ['ignore', 'pipe', 'pipe'],

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Map to plain data before recording: Date to toISOString(), Map to [...map.entries()], Buffer to base64/hex strings
  2. Keep evidence payloads typed as plain JSON structures (type Payload = JsonValue) so class instances fail at compile time
  3. Add a development-mode assertion that walks the payload and verifies every object's prototype

Example fix

// before
canonicalJson({ createdAt: new Date(), meta: new Map([['k', 1]]) });

// after
canonicalJson({ createdAt: new Date().toISOString(), meta: [['k', 1]] });
Defensive patterns

Strategy: type-guard

Validate before calling

function toPlainJson(value: unknown): unknown {
  if (value instanceof Date) return value.toISOString();
  if (value instanceof Map) return [...value.entries()].map(([k, v]) => [k, toPlainJson(v)]);
  if (value instanceof Set) return [...value].map(toPlainJson);
  if (Array.isArray(value)) return value.map(toPlainJson);
  if (typeof value === 'object' && value !== null) {
    if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
      return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toPlainJson(v)]));
    }
    return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toPlainJson(v)]));
  }
  return value;
}

Type guard

function isPlainJsonValue(value: unknown): value is CanonicalJsonValue {
  if (value === null || ['boolean', 'number', 'string'].includes(typeof value)) return true;
  if (Array.isArray(value)) return value.every(isPlainJsonValue);
  if (typeof value === 'object') {
    const proto = Object.getPrototypeOf(value);
    return (proto === Object.prototype || proto === null) && Object.values(value).every(isPlainJsonValue);
  }
  return false;
}

Try / catch

try {
  return canonicalJson(payload);
} catch (error) {
  if (error instanceof Error && error.message === 'canonical JSON supports only arrays and plain objects') {
    return canonicalJson(toPlainJson(payload));
  }
  throw error;
}

Prevention

When it happens

Trigger: Evidence payloads embedding new Date(...) fields, Map/Set metadata, Buffers from file reads, or domain-class instances passed directly from business logic; objects created with Object.create(null) are the only exotic form allowed.

Common situations: Reusing domain models as receipts/evidence; adding createdAt: new Date() instead of an ISO string; node Buffers placed into manifests; ORM row objects that carry constructor prototypes.

Related errors


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