paperclipai/paperclip · error

CreateOS requires an API URL.

Error message

CreateOS requires an API URL.

What it means

parseConfig requires a non-empty apiUrl to construct the CreateOS client; without it no API calls can be made. This error is thrown when the apiUrl key is absent, null, or undefined in the raw config (text() returned null for apiUrl), before any URL validation happens.

Solutions

  1. Set apiUrl in the plugin config (or the underlying env var the config is sourced from) to the CreateOS base URL, e.g. https://createos.example.com.
  2. Verify the board/plugin configuration UI actually persisted the apiUrl field and re-save it.
  3. Check for key renames between plugin versions; migrate old config keys to apiUrl.
  4. Run onEnvironmentValidateConfig before deploy to catch missing fields early.

Example fix

// before
const config = parseConfig({ apiKey: key }); // apiUrl missing
// after
const config = parseConfig({ apiUrl: process.env.CREATEOS_API_URL, apiKey: key });
Defensive patterns

Strategy: validation

Validate before calling

function requireApiUrl(raw) {
  if (raw.apiUrl == null || raw.apiUrl === "")
    throw new Error("CreateOS apiUrl is required; set it in plugin config or CREATEOS_API_URL.");
  return raw;
}

Try / catch

try {
  config = parseConfig(raw);
} catch (err) {
  if (err.message === "CreateOS requires an API URL.")
    throw new Error("CreateOS plugin not configured: set apiUrl (e.g. https://createos.example.com) in the board plugin settings.");
  throw err;
}

Prevention

When it happens

Trigger: Calling createClient/client/parseConfig/onEnvironmentProbe with a config object that omits apiUrl, or where raw.apiUrl is null/undefined (unset env var, missing JSON key, empty secret).

Common situations: Fresh install where CREATEOS_API_URL was never set, config stored per-company but never filled in on the board, renamed config key after an upgrade, or the secrets sync dropped the field.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/19cce6bb01c64df2. Report an issue: GitHub.

Appendix: source

Thrown at packages/plugins/sandbox-providers/createos/src/config.ts:21

  apiKey: string | null;
  shape: string;
  rootfs: string | null;
  region: string | null;
  timeoutMs: number;
  reuseLease: boolean;
}

export function parseConfig(raw: Record<string, unknown>): CreateosConfig {
  const text = (key: string): string | null => {
    const value = raw[key];
    if (value == null) return null;
    if (typeof value !== "string" || !value.trim() || value.includes("\0")) {
      throw new Error(`${key} must be a non-empty string.`);
    }
    return value.trim();
  };
  const apiUrl = text("apiUrl");
  if (!apiUrl) throw new Error("CreateOS requires an API URL.");
  let url: URL;
  try { url = new URL(apiUrl); } catch { throw new Error("CreateOS API URL is invalid."); }
  // Configuration is board-owned, but never follow redirects with the API key.
  // Plain HTTP is useful for a loopback development server only.
  const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
  if ((url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ||
      url.username || url.password || url.search || url.hash ||
      !["", "/", "/v1", "/v1/"].includes(url.pathname)) {
    throw new Error("CreateOS API URL must be an HTTPS origin (optionally ending in /v1); HTTP is allowed on loopback only.");
  }
  const shape = text("shape");
  if (!shape) throw new Error("CreateOS requires a shape from its shape catalog.");
  const timeoutMs = raw.timeoutMs ?? 300_000;
  if (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86_400_000) {
    throw new Error("timeoutMs must be an integer between 1 and 86400000.");
  }
  if (raw.reuseLease != null && typeof raw.reuseLease !== "boolean") {
    throw new Error("reuseLease must be a boolean.");

View on GitHub (pinned to 3f1d897a7c)