{"record":{"id":"edbba1276f795ae6","repo":"JuliusBrussee/caveman","slug":"caveman-agent-value-is-not-canonically-serializab","errorCode":null,"errorMessage":"caveman agent: value is not canonically serializable","messagePattern":"caveman agent: value is not canonically serializable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/context-ir.ts","lineNumber":337,"sourceCode":"async function sourceBytes(source: string | FileSource, rootDir: string): Promise<Uint8Array> {\n  if (typeof source === \"string\") return new TextEncoder().encode(source);\n  const fullPath = resolve(rootDir, source.path);\n  const relativePath = relative(rootDir, fullPath);\n  if (relativePath === \"..\" || relativePath.startsWith(\"../\") ||\n      relativePath.startsWith(\"..\\\\\") || isAbsolute(relativePath)) {\n    throw new Error(\"caveman agent: file source escapes project root\");\n  }\n  return new Uint8Array(await readFile(fullPath));\n}\n\nfunction encodeCanonical(value: unknown): Uint8Array {\n  return new TextEncoder().encode(stableStringify(value));\n}\n\nexport function stableStringify(value: unknown): string {\n  if (value === null || typeof value !== \"object\") {\n    const encoded = JSON.stringify(value);\n    if (encoded === undefined) throw new Error(\"caveman agent: value is not canonically serializable\");\n    return encoded;\n  }\n  if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n  const object = value as Record<string, unknown>;\n  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(\",\")}}`;\n}\n\nexport function sha256(value: Uint8Array | string): string {\n  return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nexport function opaquePayload(input: Uint8Array): boolean {\n  const value = new TextDecoder().decode(input).trim();\n  if (/^[-]{5}BEGIN (?:PGP SIGNED MESSAGE|[A-Z ]+ SIGNATURE)[-]{5}/.test(value) ||\n      /^[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}$/.test(value) ||\n      /\\b(?:x-amz-signature|signature)=([0-9a-f]{32,}|[A-Za-z0-9_-]{32,})\\b/i.test(value)) {\n    return true;\n  }","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/context-ir.ts#L319-L355","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst record = { name: \"tool\", handler: myFn, notes: undefined };\ncontractSHA256(record);\n// after\nconst record = { name: \"tool\", handlerName: myFn.name };\ncontractSHA256(record);","handlingStrategy":"validation","validationCode":"function isCanonicallySerializable(value: unknown): boolean {\n  if (value === null) return true;\n  if (typeof value !== \"object\") return typeof value !== \"function\" && typeof value !== \"symbol\" && value !== undefined;\n  if (Array.isArray(value)) return value.every(isCanonicallySerializable);\n  return Object.values(value).every(isCanonicallySerializable);\n}\nif (!isCanonicallySerializable(record)) throw new Error(\"record contains undefined/function/symbol\");","typeGuard":"function isPlainValue(v: unknown): v is string | number | boolean | null | CanonObject | CanonArray {\n  return v === null || [\"string\", \"number\", \"boolean\"].includes(typeof v) ||\n    (Array.isArray(v) && v.every(isPlainValue)) ||\n    (typeof v === \"object\" && Object.values(v).every(isPlainValue));\n}","tryCatchPattern":"try {\n  const hash = contractSHA256(record);\n} catch (e) {\n  if (e instanceof Error && e.message.includes(\"not canonically serializable\")) {\n    record = JSON.parse(JSON.stringify(record, (_k, v) => v === undefined ? null : (typeof v === \"function\" ? String(v.name) : v)));\n  } else throw e;\n}","preventionTips":["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."],"tags":["serialization","json","hashing"],"backgroundTag":"json-serialization-failed","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}