{"record":{"id":"a8b428f29fa75d63","repo":"JuliusBrussee/caveman","slug":"cave-gateway-required-for-locked-plan","errorCode":"cave_gateway_required_for_locked_plan","errorMessage":"cave_gateway_required_for_locked_plan: Cave Build execution routes through the Caveman gateway; run unlocked for observe-only","messagePattern":"cave_gateway_required_for_locked_plan: Cave Build execution routes through the Caveman gateway; run unlocked for observe-only","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":5094,"sourceCode":"    billingProofRequired?: boolean;\n  },\n  planPresent: boolean,\n): Promise<ResolvedCaveRoute> {\n  if (options.caveRoute !== undefined) {\n    if (!options.billingProofRequired || !options.caveRoute.useGateway ||\n        options.caveRoute.providerBilling === \"managed\" ||\n        options.caveRoute.providerBilling === \"byok\") {\n      return options.caveRoute;\n    }\n    const identity = await gatewayIdentity(gatewayURL, options.fetch ?? globalThis.fetch);\n    return {\n      ...options.caveRoute,\n      providerBilling: identity?.providerBilling ?? \"unknown\",\n    };\n  }\n  if (options.cave === \"off\") {\n    if (planPresent) {\n      throw new Error(\n        \"cave_gateway_required_for_locked_plan: Cave Build execution routes through the Caveman gateway; run unlocked for observe-only\",\n      );\n    }\n    return { useGateway: false, providerBilling: \"unknown\" };\n  }\n  if (options.ensureRuntime === false) {\n    try {\n      const url = new URL(gatewayURL);\n      if (isLoopbackHostname(url.hostname)) {\n        const providerBilling = options.billingProofRequired\n          ? (await gatewayIdentity(gatewayURL, options.fetch ?? globalThis.fetch))?.providerBilling ??\n            \"unknown\"\n          : \"unknown\";\n        return { useGateway: true, providerBilling };\n      }\n      // Remote gateways still have to prove Caveman identity. `ensureCaveRuntime`\n      // never starts a process for non-loopback URLs; it only enforces HTTPS and\n      // performs the content-blind ownership handshake.","sourceCodeStart":5076,"sourceCodeEnd":5112,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/runtime.ts#L5076-L5112","documentation":"Thrown by route resolution when RunOptions.cave is explicitly \"off\" while a locked build or candidate plan is attached to the run. Locked-plan execution must route through the Caveman gateway, because billing/evidence (the x-cave-* headers and account key) are only honest when the gateway proxies the request; observe-only passthrough would silently under-claim, so the run fails closed instead of degrading. Unlocked runs with cave \"off\" are fine and simply report RunResult.mode \"observe-only\".","triggerScenarios":"Calling the agent run API with a locked plan / candidate plan present (planPresent true) while options.cave === \"off\" — e.g. run(agent, { plan: lockedPlan, cave: 'off' }). The check fires before any probe: cave is off AND a plan is attached.","commonSituations":"CI pipelines that disable the gateway with cave:'off' but still execute a compiled plan; developers copying an options object that pins cave:'off' from an observe-only experiment and then attaching a locked build; an env/config toggle that maps to cave off while the build artifact is auto-loaded.","solutions":["Remove cave:'off' and let the default ensureRuntime auto-start/attach the loopback Caveman gateway (caveman setup --install && caveman start once to install the runtime)","Start the gateway before the run (caveman start) and verify with caveman doctor that locked-execution readiness is true","If you intentionally cannot use a gateway, drop the locked plan / candidate plan from the call and run unlocked (observe-only) instead","In the coding-session surface, this exact error earns the built-in single retry without the plan — rely on that only if observe-only is acceptable"],"exampleFix":"// before\nawait run(agent, { plan: lockedPlan, cave: 'off' }); // throws cave_gateway_required_for_locked_plan\n\n// after — option 1: let the runtime ensure the loopback gateway (default)\nawait run(agent, { plan: lockedPlan });\n\n// after — option 2: intentionally unlocked, observe-only\nawait run(agent, { cave: 'off' });","handlingStrategy":"fallback","validationCode":"async function gatewayReady(gatewayURL, fetchImpl = fetch) {\n  try {\n    const res = await fetchImpl(gatewayURL.replace(/\\/$/, '') + '/health/ready');\n    return res.ok; // Caveman identity implied by 200 on /health/ready\n  } catch { return false; }\n}\n// before a locked run:\nif (!(await gatewayReady(gatewayURL))) await startGateway();","typeGuard":"function runsLockedPlan(options) {\n  return Boolean(options.plan ?? options.build ?? options.lockedBuild);\n}","tryCatchPattern":"try {\n  await run(agent, options);\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith('cave_gateway_required_for_locked_plan')) {\n    // same policy as the coding-session surface: exactly one retry, unlocked (observe-only)\n    return run(agent, { ...options, plan: undefined });\n  }\n  throw error;\n}","preventionTips":["Never combine cave:'off' with a locked plan in CI configs — assert one or the other in a config lint","Run caveman doctor as a CI step; treat locked-execution readiness false as a skipped-not-failed locked lane","Install/ensure the runtime once per machine (caveman setup --install) so the default ensureRuntime path works"],"tags":["gateway","locked-plan","cave-routing","configuration"],"backgroundTag":"locked-plan-requires-gateway","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"}