paperclipai/paperclip · error

CreateOS API URL must be an HTTPS origin (optionally ending…

Error message

CreateOS API URL must be an HTTPS origin (optionally ending in /v1); HTTP is allowed on loopback only.

What it means

parseConfig in the CreateOS sandbox plugin validates the configured apiUrl before the plugin ever talks to the remote service. The URL must be an HTTPS origin (optionally with a trailing /v1 path), must not embed credentials, query strings, or fragments, and plain HTTP is accepted only for loopback hosts (localhost, 127.0.0.1, [::1]). This guard prevents accidentally sending the API key over unencrypted transport or to a redirecting/non-origin URL.

Solutions

  1. Change the apiUrl scheme to https:// (e.g. https://api.sb.createos.sh or https://your-host/v1).
  2. If developing locally, serve on localhost, 127.0.0.1, or [::1] so HTTP loopback is allowed.
  3. Strip credentials, query strings, and fragments from the URL; put the key in the environment config or CREATEOS_API_KEY instead.
  4. Normalize the path to just the origin or origin + /v1; put any other path prefix behind a reverse proxy on that origin.

Example fix

// before
{ "apiUrl": "http://sandbox.internal:8080/api/v1?env=dev" }
// after
{ "apiUrl": "https://sandbox.internal:8080/v1" }
Defensive patterns

Strategy: validation

Validate before calling

function isValidCreateosUrl(raw) {
  try {
    const u = new URL(raw);
    const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname);
    return (u.protocol === "https:" || (u.protocol === "http:" && loopback)) &&
      !u.username && !u.password && !u.search && !u.hash &&
      ["", "/", "/v1", "/v1/"].includes(u.pathname);
  } catch { return false; }
}

Prevention

When it happens

Trigger: Calling parseConfig (directly or via client/config construction, or the onEnvironmentValidateConfig/onEnvironmentProbe hooks) with a config whose apiUrl is http:// on a non-loopback host, uses ftp/other schemes, contains ?query, #fragment, user:pass@ credentials, or a pathname other than '', '/', '/v1', or '/v1/'.

Common situations: Local dev server on a LAN IP (http://192.168.x.x) instead of localhost; pointing at a self-hosted CreateOS instance behind plain HTTP; pasting a URL with a trailing query string or embedded basic-auth credentials; pointing at a proxy path like /api/v2.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


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

Appendix: source

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

  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.");
  }
  return {
    apiUrl: url.origin,
    apiKey: text("apiKey"),
    shape,
    rootfs: text("rootfs"),
    region: text("region"),
    timeoutMs,
    reuseLease: raw.reuseLease === true,

View on GitHub (pinned to 3f1d897a7c)