JuliusBrussee/caveman · error

cave_gateway_identity_unverified

cave_gateway_identity_unverified

Error message

cave_gateway_identity_unverified: non-loopback gateway ${gatewayURL} requires https

What it means

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.

Source

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

  });
  return probe;
}

export async function ensureCaveRuntime(
  gatewayURL: string,
  fetchImpl: typeof globalThis.fetch,
): Promise<GatewayProviderBilling> {
  const url = new URL(gatewayURL);
  const loopback = isLoopbackHostname(url.hostname);
  if (!loopback) {
    // A non-loopback gateway is never taken on faith: routing there sends the
    // provider credential plus every x-cave-* header to that host, so it must
    // prove Caveman identity before it gets any traffic. Plain http cannot
    // authenticate the peer at all, and an https host that fails the
    // /health/ready identity handshake is equally unverified — both fail
    // closed here so resolveCaveRoute degrades to observe-only passthrough.
    if (url.protocol !== "https:") {
      throw new Error(
        `cave_gateway_identity_unverified: non-loopback gateway ${gatewayURL} requires https`,
      );
    }
    const identity = await gatewayIdentity(gatewayURL, fetchImpl);
    if (identity !== undefined) return identity.providerBilling;
    throw new Error(
      `cave_gateway_identity_unverified: ${gatewayURL}/health/ready did not identify as caveman-proxy`,
    );
  }
  const readyBilling = await runtimeReady(gatewayURL, fetchImpl);
  if (readyBilling !== undefined) return readyBilling;

  const command = process.env.CAVEMAN_CLI_BIN ?? "caveman";
  let startupFailure: Error | undefined;
  let invocation;
  try {
    invocation = portableInvocation(command, ["start"], { env: buildRuntimeControlEnv() });
  } catch (error) {

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Use an https:// URL for any non-loopback gateway (terminate TLS at the gateway or a reverse proxy in front of it)
  2. For pure local development, point the gateway URL at a loopback hostname (127.0.0.1 / localhost) — loopback is exempt from the https requirement
  3. Verify the URL actually typed/parsed: a missing scheme defaulting to http:, or a proxy env var rewriting the URL, hits the same check

Example fix

// before
const gateway = 'http://cave.internal.corp:8080'; // non-loopback, plain http

// after
const gateway = 'https://cave.internal.corp:8443'; // or 'http://127.0.0.1:8080' for loopback dev
Defensive patterns

Strategy: validation

Validate before calling

const LOOPBACK = new Set(['127.0.0.1', '::1', 'localhost']);
function validateGatewayURL(gatewayURL) {
  const url = new URL(gatewayURL); // throws on malformed URL too
  if (!LOOPBACK.has(url.hostname) && url.protocol !== 'https:') {
    throw new Error(`non-loopback gateway must use https: ${gatewayURL}`);
  }
  return url;
}

Type guard

function isLoopbackHostname(hostname) {
  return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
}

Try / catch

try {
  await run(agent, options);
} catch (error) {
  if (error instanceof Error && error.message.includes('requires https')) {
    options.caveRoute = { ...options.caveRoute, gatewayURL: toHttps(options.caveRoute.gatewayURL) };
    return run(agent, options);
  }
  throw error;
}

Prevention

When it happens

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

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

Related errors


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