{"record":{"id":"a497b723a93beb3c","repo":"JuliusBrussee/caveman","slug":"cave-sandbox-failure-not-serializable","errorCode":"cave_sandbox_failure_not_serializable","errorMessage":"cave_sandbox_failure_not_serializable","messagePattern":"cave_sandbox_failure_not_serializable","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/tool-worker.ts","lineNumber":26,"sourceCode":"import type { AgentDefinition } from \"./index.js\";\nimport { installNetworkDeny } from \"./sandbox-network.js\";\n\n// The result travels on a DEDICATED fd 3, length-prefixed — never stdout\n// Any console.log in the tool's own import graph writes to stdout\n// (fd 1) and the parent ignores it, so it can no longer collide with the result\n// JSON and corrupt it into cave_sandbox_invalid_output.\nlet resultWritten = false;\nfunction writeResult(payload: { ok: boolean; value?: unknown; code?: string }): void {\n  if (resultWritten) return;\n  let encoded: string;\n  try {\n    encoded = JSON.stringify(payload);\n  } catch {\n    if (payload.ok) {\n      writeResult({ ok: false, code: \"cave_sandbox_result_not_serializable\" });\n      return;\n    }\n    throw new Error(\"cave_sandbox_failure_not_serializable\");\n  }\n  // Mark terminal only after serialization succeeds. Otherwise a BigInt or\n  // circular tool value suppresses the failure frame and becomes an opaque\n  // cave_sandbox_invalid_output at the parent.\n  resultWritten = true;\n  const body = Buffer.from(encoded, \"utf8\");\n  const header = Buffer.allocUnsafe(4);\n  header.writeUInt32BE(body.byteLength, 0);\n  const frame = Buffer.concat([header, body]);\n  let offset = 0;\n  while (offset < frame.byteLength) {\n    offset += writeSync(3, frame, offset, frame.byteLength - offset);\n  }\n}\n\nfunction failureCode(error: unknown): string {\n  return error instanceof Error\n    ? [","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/tool-worker.ts#L8-L44","documentation":"The tool worker writes exactly one result frame to fd 3; if serializing a FAILURE payload ({ ok: false, ... }) with JSON.stringify throws (e.g. a non-serializable property like BigInt or a circular reference on the error), writeResult throws cave_sandbox_failure_not_serializable — it cannot even produce a fallback failure frame. This is a last-resort invariant: terminal state is only marked after serialization succeeds so a bad value can never corrupt the protocol into an opaque invalid-output.","triggerScenarios":"A sandboxed tool throws an Error whose own properties (message, cause, custom fields) contain BigInt, circular structures, or other JSON-hostile values carried onto the failure payload.","commonSituations":"Custom error classes attaching the offending value itself (e.g. error.value = someBigInt); rethrowing provider/tool objects with cycles; almost never hit because success payloads convert to cave_sandbox_result_not_serializable instead.","solutions":["Sanitize thrown errors in tool code: keep message a string and drop or String() non-serializable custom properties.","If you attach context to errors, attach primitives or serializable summaries only.","Reproduce locally by running the tool in-process and inspecting the thrown error's own enumerable properties."],"exampleFix":"// before\nthrow Object.assign(new Error(\"bad value\"), { value: 10n });\n\n// after\nthrow new Error(`bad value: ${10n}`);","handlingStrategy":"fallback","validationCode":"function serializableError(err: unknown): boolean {\n  try { JSON.stringify({ m: (err as Error)?.message, c: (err as Error)?.cause }); return true; }\n  catch { return false; }\n}","typeGuard":"function isSerializable(value: unknown): boolean {\n  try { JSON.stringify(value); return true; } catch { return false; }\n}","tryCatchPattern":"// In tool code — sanitize before throwing from a sandboxed worker:\ntry {\n  risky();\n} catch (error) {\n  throw new Error(error instanceof Error ? String(error.message) : String(error));\n}","preventionTips":["Throw plain Error objects with string messages from sandboxed tools.","Never attach BigInt, circular structures, or live handles to error properties.","Stringify non-serializable context before putting it on an error."],"tags":["sandbox","worker","serialization","errors"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}