JuliusBrussee/caveman · error

cave_internal_run_option

cave_internal_run_option

Error message

cave_internal_run_option: Cave Build execution is available through caveman-agent dev/build after lock validation

What it means

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.

Source

Thrown at packages/agent/src/runtime.ts:5003

    }
  }
  return false;
}

/**
 * Refuse caller-supplied build identity, plan, or routing.
 *
 * Exported for the other public entry points that forward caller options into
 * `runAgentInternal` (`./code`): the guard belongs to whoever accepts the
 * untrusted object, and it must run BEFORE any session-internal field is merged
 * in, or the session's own plan and route would trip it.
 */
export function rejectInternalRunOptions(options: RunOptions): void {
  const value = options as Record<string, unknown>;
  if ("buildIdentity" in value || "efficiencyPlan" in value ||
      "lockedBuild" in value || "candidatePlan" in value || "caveRoute" in value ||
      "invocationTrace" in value || "invocationState" in value) {
    throw new Error(
      "cave_internal_run_option: Cave Build execution is available through caveman-agent dev/build after lock validation",
    );
  }
}

function sandboxDependencyReadRoots(): string[] {
  const resolved = [
    import.meta.resolve("@earendil-works/pi-agent-core"),
    import.meta.resolve("@earendil-works/pi-ai"),
  ].map((url) => fileURLToPath(url));
  const roots = new Set<string>();
  for (const path of resolved) {
    const pnpm = path.indexOf("/node_modules/.pnpm/");
    if (pnpm >= 0) {
      roots.add(path.slice(0, pnpm + "/node_modules/.pnpm".length));
    } else {
      roots.add(dirname(dirname(path)));
    }

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Remove the internal fields; build options from your own typed RunOptions literal
  2. Use the caveman-agent dev/build CLI path for locked-plan execution - it performs lock validation first
  3. Never round-trip option objects captured from internal sessions into public calls
  4. If a wrapper proxies fields blindly, filter to a known allow-list of public keys before forwarding

Example fix

// before
await run(agent, { ...capturedInternalOptions, input });

// after: only public fields
await run(agent, { model, signal, maxSubagentDepth: 2, input });
Defensive patterns

Strategy: type-guard

Type guard

// Reject (or strip) session-internal fields before calling public run APIs
const INTERNAL_RUN_OPTION_KEYS = ['buildIdentity', 'efficiencyPlan', 'lockedBuild', 'candidatePlan', 'caveRoute', 'invocationTrace', 'invocationState'] as const;
function isPublicRunOptions(options: Record<string, unknown>): boolean {
  return !INTERNAL_RUN_OPTION_KEYS.some((key) => key in options);
}
function toPublicRunOptions<T extends object>(options: T): T {
  const copy: Record<string, unknown> = { ...options };
  for (const key of INTERNAL_RUN_OPTION_KEYS) delete copy[key];
  return copy as T;
}

Try / catch

try {
  return await run(agent, options);
} catch (error) {
  if (error instanceof Error && error.message.startsWith('cave_internal_run_option')) {
    return await run(agent, toPublicRunOptions(options)); // retry with internal fields stripped
  }
  throw error;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/f4087f18146a29aa. Report an issue: GitHub.