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
- Map to plain data before recording: Date to toISOString(), Map to [...map.entries()], Buffer to base64/hex strings
- Keep evidence payloads typed as plain JSON structures (type Payload = JsonValue) so class instances fail at compile time
- 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
- Convert Date to ISO string and Map/Set to arrays at the point evidence is assembled
- Never pass ORM entities or class instances; map to plain records first
- Keep a PlainJson<T> type alias for all hashed payloads
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
- canonical JSON does not support lone UTF-16 surrogates
- canonical JSON requires finite, safe, non-negative-zero numb
- canonical JSON does not support undefined
- canonical JSON does not support ${typeof item}
- canonical JSON does not support cycles
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/cb37ea45f56a7201.
Report an issue: GitHub.