{"record":{"id":"f4087f18146a29aa","repo":"JuliusBrussee/caveman","slug":"cave-internal-run-option","errorCode":"cave_internal_run_option","errorMessage":"cave_internal_run_option: Cave Build execution is available through caveman-agent dev/build after lock validation","messagePattern":"cave_internal_run_option: Cave Build execution is available through caveman-agent dev/build after lock validation","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":5003,"sourceCode":"    }\n  }\n  return false;\n}\n\n/**\n * Refuse caller-supplied build identity, plan, or routing.\n *\n * Exported for the other public entry points that forward caller options into\n * `runAgentInternal` (`./code`): the guard belongs to whoever accepts the\n * untrusted object, and it must run BEFORE any session-internal field is merged\n * in, or the session's own plan and route would trip it.\n */\nexport function rejectInternalRunOptions(options: RunOptions): void {\n  const value = options as Record<string, unknown>;\n  if (\"buildIdentity\" in value || \"efficiencyPlan\" in value ||\n      \"lockedBuild\" in value || \"candidatePlan\" in value || \"caveRoute\" in value ||\n      \"invocationTrace\" in value || \"invocationState\" in value) {\n    throw new Error(\n      \"cave_internal_run_option: Cave Build execution is available through caveman-agent dev/build after lock validation\",\n    );\n  }\n}\n\nfunction sandboxDependencyReadRoots(): string[] {\n  const resolved = [\n    import.meta.resolve(\"@earendil-works/pi-agent-core\"),\n    import.meta.resolve(\"@earendil-works/pi-ai\"),\n  ].map((url) => fileURLToPath(url));\n  const roots = new Set<string>();\n  for (const path of resolved) {\n    const pnpm = path.indexOf(\"/node_modules/.pnpm/\");\n    if (pnpm >= 0) {\n      roots.add(path.slice(0, pnpm + \"/node_modules/.pnpm\".length));\n    } else {\n      roots.add(dirname(dirname(path)));\n    }","sourceCodeStart":4985,"sourceCodeEnd":5021,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/runtime.ts#L4985-L5021","documentation":"Public run entry points reject RunOptions carrying session-internal fields: buildIdentity, efficiencyPlan, lockedBuild, candidatePlan, caveRoute, invocationTrace, invocationState. Those fields drive Cave Build execution, which is reachable only through caveman-agent dev/build after lock validation; the guard runs before any session-internal field is merged so a caller cannot forge plan/routing identity into a public call. The message states where the capability actually lives.","triggerScenarios":"Passing an options object containing any of the seven internal keys to a public run/invoke API; spreading a previously captured internal options object (e.g., observed from a session turn) back into a public call; trying to hand-unlock locked-build execution by constructing a lockedBuild/candidatePlan value.","commonSituations":"Spreading unknown or round-tripped objects into options ({...captured, input}); copying example/test fixtures that included internal fields; upgrading to a version where previously-ignored extra fields became a hard rejection; SDK wrappers that proxy every field through.","solutions":["Remove the internal fields; build options from your own typed RunOptions literal","Use the caveman-agent dev/build CLI path for locked-plan execution - it performs lock validation first","Never round-trip option objects captured from internal sessions into public calls","If a wrapper proxies fields blindly, filter to a known allow-list of public keys before forwarding"],"exampleFix":"// before\nawait run(agent, { ...capturedInternalOptions, input });\n\n// after: only public fields\nawait run(agent, { model, signal, maxSubagentDepth: 2, input });","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// Reject (or strip) session-internal fields before calling public run APIs\nconst INTERNAL_RUN_OPTION_KEYS = ['buildIdentity', 'efficiencyPlan', 'lockedBuild', 'candidatePlan', 'caveRoute', 'invocationTrace', 'invocationState'] as const;\nfunction isPublicRunOptions(options: Record<string, unknown>): boolean {\n  return !INTERNAL_RUN_OPTION_KEYS.some((key) => key in options);\n}\nfunction toPublicRunOptions<T extends object>(options: T): T {\n  const copy: Record<string, unknown> = { ...options };\n  for (const key of INTERNAL_RUN_OPTION_KEYS) delete copy[key];\n  return copy as T;\n}","tryCatchPattern":"try {\n  return await run(agent, options);\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith('cave_internal_run_option')) {\n    return await run(agent, toPublicRunOptions(options)); // retry with internal fields stripped\n  }\n  throw error;\n}","preventionTips":["Construct RunOptions from typed literals; never spread captured or unknown objects into them","Do not round-trip option objects observed inside sessions back into public calls","Use the caveman-agent dev/build CLI for locked-plan execution - it performs lock validation","In wrappers, forward an explicit allow-list of public keys instead of proxying every field"],"tags":["api-contract","security","internal-options","fail-closed"],"backgroundTag":"forbidden-option-field","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}