JuliusBrussee/caveman · error

private device login requires a keyless project grant with…

Error message

private device login requires a keyless project grant with a refresh token and delivery acknowledgement

What it means

For private instances the CLI enforces that the token response is a 'keyless project grant': no gateway API key material, a non-empty refresh_token, project_id, delivery_ack_token, and a scope without write privileges (proxy:write / sdk:write). If the status is not 2xx or any invariant fails, this error is thrown. It guards private instances from receiving over-privileged or undeliverable credentials.

Solutions

  1. Configure the private instance's authorization server to issue keyless project grants (credential_kind "none") with refresh tokens
  2. Narrow the issued scopes so they exclude proxy:write and sdk:write
  3. Ensure the token response includes non-empty refresh_token, project_id, delivery_ack_token, and scope
  4. Verify the --instance points at the correct, properly configured authorization server

Example fix

// before
{ "credential_kind": "gateway_api_key", "gateway_api_key": "...", "scope": "proxy:write" }
// after
{ "credential_kind": "none", "refresh_token": "...", "project_id": "...", "delivery_ack_token": "...", "scope": "read" }
Defensive patterns

Strategy: validation

Validate before calling

function isValidKeylessGrant(tok) {
  return tok.credential_kind === "none" && typeof tok.refresh_token === "string" && tok.refresh_token &&
    typeof tok.project_id === "string" && tok.project_id && typeof tok.delivery_ack_token === "string" && tok.delivery_ack_token &&
    typeof tok.scope === "string" && tok.scope && !tok.scope.split(/\s+/).some(s => s === "proxy:write" || s === "sdk:write");
}

Type guard

function isKeylessGrant(tok) { return !!tok && tok.credential_kind === "none" && !tok.gateway_api_key && !tok.gateway_key_id && !tok.gateway_url; }

Try / catch

try { await login({ instance }) } catch (e) { if (e.message.includes("keyless project grant")) console.error("Instance issued an over-privileged or incomplete token; fix the authorization server config"); }

Prevention

When it happens

Trigger: A private-instance device token poll returns a payload where credential_kind is not "none", any gateway_api_key/gateway_key_id/gateway_url is present, refresh_token/project_id/delivery_ack_token/scope is missing or empty, or the scope contains proxy:write or sdk:write; or the HTTP status is outside 200–299.

Common situations: Authorization server misconfigured to issue gateway API keys for private instances; missing refresh-token grant on the client; overly broad scope configuration on the server; wrong audience/authorization server returning a cloud-style token bundle.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/c0b1a795d9277fea. Report an issue: GitHub.

Appendix: source

Thrown at packages/cli/src/index.ts:9737

      }
      await sleep(Math.max(intervalMs, retryAfterMs, 200));
      continue;
    }
    if (tokenStatus >= 300 && tokenStatus < 400) throw new Error("device login refused a redirected token endpoint");
    if (tokenStatus === 429) {
      // rateLimitAuth returns a nested cave error envelope rather than the RFC
      // `error` string. Status is the authoritative retry signal here.
      await sleep(Math.max(intervalMs, retryAfterMs, 200));
      continue;
    }
    const accessToken = typeof tok.access_token === "string" ? tok.access_token : "";
    if (accessToken) {
	  if (instance && (tokenStatus < 200 || tokenStatus >= 300 || tok.credential_kind !== "none" ||
	      ["gateway_api_key", "gateway_key_id", "gateway_url"].some((key) => tok[key] != null) ||
	      typeof tok.refresh_token !== "string" || !tok.refresh_token || typeof tok.project_id !== "string" || !tok.project_id ||
	      typeof tok.delivery_ack_token !== "string" || !tok.delivery_ack_token || typeof tok.scope !== "string" || !tok.scope ||
	      tok.scope.split(/\s+/).some((scope) => scope === "proxy:write" || scope === "sdk:write"))) {
	    throw new Error("private device login requires a keyless project grant with a refresh token and delivery acknowledgement");
	  }
	  const credentials: StoredCredentials = {
	    access_token: accessToken,
	    ...(typeof tok.refresh_token === "string" && tok.refresh_token ? { refresh_token: tok.refresh_token } : {}),
	    ...(typeof tok.gateway_api_key === "string" && tok.gateway_api_key ? { gateway_api_key: tok.gateway_api_key } : {}),
	    ...(typeof tok.gateway_key_id === "string" && tok.gateway_key_id ? { gateway_key_id: tok.gateway_key_id } : {}),
	    ...(typeof tok.project_id === "string" && tok.project_id ? { project_id: tok.project_id } : {}),
	  };
	  const tokenStore = storeCredentials(credentials);
	  const organizationId = orgFromToken(accessToken);
	  const gateway = instance ? "" : resolveLoginGatewayUrl(baseURL, tok, code, argv);
	  const saved: Config = { baseURL, token: "", tokenStore };
	  if (organizationId) saved.organizationId = organizationId;
	  if (credentials.project_id) saved.projectId = credentials.project_id;
	  if (gateway) saved.gatewayUrl = gateway;
	  // Persist the complete local login state before the server-side receipt fence:
	  // an ACK may permanently purge the replay bundle, so a config write that fails
	  // must leave the grant retryable rather than acknowledging an undiscoverable

View on GitHub (pinned to 3ee70a1026)