{"record":{"id":"036c640dad80319c","repo":"JuliusBrussee/caveman","slug":"cave-gateway-identity-unverified","errorCode":"cave_gateway_identity_unverified","errorMessage":"cave_gateway_identity_unverified: non-loopback gateway ${gatewayURL} requires https","messagePattern":"cave_gateway_identity_unverified: non-loopback gateway (.+?) requires https","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":5221,"sourceCode":"  });\n  return probe;\n}\n\nexport async function ensureCaveRuntime(\n  gatewayURL: string,\n  fetchImpl: typeof globalThis.fetch,\n): Promise<GatewayProviderBilling> {\n  const url = new URL(gatewayURL);\n  const loopback = isLoopbackHostname(url.hostname);\n  if (!loopback) {\n    // A non-loopback gateway is never taken on faith: routing there sends the\n    // provider credential plus every x-cave-* header to that host, so it must\n    // prove Caveman identity before it gets any traffic. Plain http cannot\n    // authenticate the peer at all, and an https host that fails the\n    // /health/ready identity handshake is equally unverified — both fail\n    // closed here so resolveCaveRoute degrades to observe-only passthrough.\n    if (url.protocol !== \"https:\") {\n      throw new Error(\n        `cave_gateway_identity_unverified: non-loopback gateway ${gatewayURL} requires https`,\n      );\n    }\n    const identity = await gatewayIdentity(gatewayURL, fetchImpl);\n    if (identity !== undefined) return identity.providerBilling;\n    throw new Error(\n      `cave_gateway_identity_unverified: ${gatewayURL}/health/ready did not identify as caveman-proxy`,\n    );\n  }\n  const readyBilling = await runtimeReady(gatewayURL, fetchImpl);\n  if (readyBilling !== undefined) return readyBilling;\n\n  const command = process.env.CAVEMAN_CLI_BIN ?? \"caveman\";\n  let startupFailure: Error | undefined;\n  let invocation;\n  try {\n    invocation = portableInvocation(command, [\"start\"], { env: buildRuntimeControlEnv() });\n  } catch (error) {","sourceCodeStart":5203,"sourceCodeEnd":5239,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/runtime.ts#L5203-L5239","documentation":"Thrown during gateway identity verification when the gateway URL is non-loopback and uses plain http. Routing to a host sends the provider credential plus every x-cave-* header, so a non-loopback gateway must prove Caveman identity first — and http cannot authenticate the peer at all. The check fails closed: without a plan the run degrades to observe-only; with a locked plan it surfaces wrapped as cave_gateway_required_for_locked_plan.","triggerScenarios":"ensureCaveRuntime / gatewayIdentity called with a gatewayURL whose hostname is not loopback (LAN IP, domain, 0.0.0.0, docker host name) and whose protocol is not https: — e.g. http://10.0.0.5:8080 or http://cave.internal:80.","commonSituations":"Teams pointing the framework at a shared/internal gateway over plain http on the LAN; local docker setups where the gateway is reached via the docker host name instead of 127.0.0.1; a reverse proxy terminating TLS but the configured upstream URL kept as http.","solutions":["Use an https:// URL for any non-loopback gateway (terminate TLS at the gateway or a reverse proxy in front of it)","For pure local development, point the gateway URL at a loopback hostname (127.0.0.1 / localhost) — loopback is exempt from the https requirement","Verify the URL actually typed/parsed: a missing scheme defaulting to http:, or a proxy env var rewriting the URL, hits the same check"],"exampleFix":"// before\nconst gateway = 'http://cave.internal.corp:8080'; // non-loopback, plain http\n\n// after\nconst gateway = 'https://cave.internal.corp:8443'; // or 'http://127.0.0.1:8080' for loopback dev","handlingStrategy":"validation","validationCode":"const LOOPBACK = new Set(['127.0.0.1', '::1', 'localhost']);\nfunction validateGatewayURL(gatewayURL) {\n  const url = new URL(gatewayURL); // throws on malformed URL too\n  if (!LOOPBACK.has(url.hostname) && url.protocol !== 'https:') {\n    throw new Error(`non-loopback gateway must use https: ${gatewayURL}`);\n  }\n  return url;\n}","typeGuard":"function isLoopbackHostname(hostname) {\n  return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';\n}","tryCatchPattern":"try {\n  await run(agent, options);\n} catch (error) {\n  if (error instanceof Error && error.message.includes('requires https')) {\n    options.caveRoute = { ...options.caveRoute, gatewayURL: toHttps(options.caveRoute.gatewayURL) };\n    return run(agent, options);\n  }\n  throw error;\n}","preventionTips":["Validate the gateway URL scheme at config load time, not at run time","Terminate TLS at (or in front of) any remote gateway; never route provider credentials over plain http","For local dev prefer http://127.0.0.1:port which is exempt from the https requirement"],"tags":["gateway","https","tls","identity-verification","security"],"backgroundTag":"https-required","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"}