JuliusBrussee/caveman · error
caveman agent: value is not canonically serializable
Error message
caveman agent: value is not canonically serializable
What it means
Thrown by stableStringify in packages/agent/src/context-ir.ts when a value cannot be represented in the canonical JSON form used for hashing/signing. JSON.stringify returns undefined for undefined, functions, and symbols, so such values cannot take part in a deterministic canonical record (e.g. contractSHA256).
Solutions
- Remove or omit undefined/function/symbol values from the object before hashing — use JSON.parse(JSON.stringify(...)) semantics or explicit null.
- Replace functions with serializable descriptors (e.g. the function's name or source string).
- Ensure optional fields are deleted (delete obj.key) rather than set to undefined when building canonical records.
Example fix
// before
const record = { name: "tool", handler: myFn, notes: undefined };
contractSHA256(record);
// after
const record = { name: "tool", handlerName: myFn.name };
contractSHA256(record); Defensive patterns
Strategy: validation
Validate before calling
function isCanonicallySerializable(value: unknown): boolean {
if (value === null) return true;
if (typeof value !== "object") return typeof value !== "function" && typeof value !== "symbol" && value !== undefined;
if (Array.isArray(value)) return value.every(isCanonicallySerializable);
return Object.values(value).every(isCanonicallySerializable);
}
if (!isCanonicallySerializable(record)) throw new Error("record contains undefined/function/symbol"); Type guard
function isPlainValue(v: unknown): v is string | number | boolean | null | CanonObject | CanonArray {
return v === null || ["string", "number", "boolean"].includes(typeof v) ||
(Array.isArray(v) && v.every(isPlainValue)) ||
(typeof v === "object" && Object.values(v).every(isPlainValue));
} Try / catch
try {
const hash = contractSHA256(record);
} catch (e) {
if (e instanceof Error && e.message.includes("not canonically serializable")) {
record = JSON.parse(JSON.stringify(record, (_k, v) => v === undefined ? null : (typeof v === "function" ? String(v.name) : v)));
} else throw e;
} Prevention
- Never place functions, symbols, or undefined in objects destined for hashing/signing.
- Use explicit null instead of undefined for absent optional fields.
- Sanitize with a replacer (omit undefined/functions) before computing canonical hashes.
- Add a unit test asserting every canonical record passes stableStringify.
When it happens
Trigger: Passing an object graph containing undefined, a function, or a symbol as a top-level scalar (e.g. stableStringify(undefined) or stableStringify(() => {})) to any API that computes a canonical hash: contractSHA256, canonicalRecord, signature, callSignature, evalSuiteSha256, or run.
Common situations: A tool parameter defaulted to undefined, a callback accidentally included in a contract object, a class instance with method fields, or missing optional fields that serialize as undefined instead of being omitted.
Understand the failure class
Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.
Related errors
- assembly slot content is not JSON-serializable
- assembly slot content is not JSON-serializable
- cave_sandbox_failure_not_serializable
- encode before evidence
- encode report
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/edbba1276f795ae6.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/context-ir.ts:337
async function sourceBytes(source: string | FileSource, rootDir: string): Promise<Uint8Array> {
if (typeof source === "string") return new TextEncoder().encode(source);
const fullPath = resolve(rootDir, source.path);
const relativePath = relative(rootDir, fullPath);
if (relativePath === ".." || relativePath.startsWith("../") ||
relativePath.startsWith("..\\") || isAbsolute(relativePath)) {
throw new Error("caveman agent: file source escapes project root");
}
return new Uint8Array(await readFile(fullPath));
}
function encodeCanonical(value: unknown): Uint8Array {
return new TextEncoder().encode(stableStringify(value));
}
export function stableStringify(value: unknown): string {
if (value === null || typeof value !== "object") {
const encoded = JSON.stringify(value);
if (encoded === undefined) throw new Error("caveman agent: value is not canonically serializable");
return encoded;
}
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
const object = value as Record<string, unknown>;
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
}
export function sha256(value: Uint8Array | string): string {
return createHash("sha256").update(value).digest("hex");
}
export function opaquePayload(input: Uint8Array): boolean {
const value = new TextDecoder().decode(input).trim();
if (/^[-]{5}BEGIN (?:PGP SIGNED MESSAGE|[A-Z ]+ SIGNATURE)[-]{5}/.test(value) ||
/^[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}$/.test(value) ||
/\b(?:x-amz-signature|signature)=([0-9a-f]{32,}|[A-Za-z0-9_-]{32,})\b/i.test(value)) {
return true;
}View on GitHub (pinned to 3ee70a1026)