{"record":{"id":"a9bed712e47207f5","repo":"paperclipai/paperclip","slug":"key-must-be-a-non-empty-string","errorCode":null,"errorMessage":"${key} must be a non-empty string.","messagePattern":"(.+?) must be a non-empty string\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/createos/src/config.ts","lineNumber":16,"sourceCode":"export interface CreateosConfig {\n  apiUrl: string;\n  apiKey: string | null;\n  shape: string;\n  rootfs: string | null;\n  region: string | null;\n  timeoutMs: number;\n  reuseLease: boolean;\n}\n\nexport function parseConfig(raw: Record<string, unknown>): CreateosConfig {\n  const text = (key: string): string | null => {\n    const value = raw[key];\n    if (value == null) return null;\n    if (typeof value !== \"string\" || !value.trim() || value.includes(\"\\0\")) {\n      throw new Error(`${key} must be a non-empty string.`);\n    }\n    return value.trim();\n  };\n  const apiUrl = text(\"apiUrl\");\n  if (!apiUrl) throw new Error(\"CreateOS requires an API URL.\");\n  let url: URL;\n  try { url = new URL(apiUrl); } catch { throw new Error(\"CreateOS API URL is invalid.\"); }\n  // Configuration is board-owned, but never follow redirects with the API key.\n  // Plain HTTP is useful for a loopback development server only.\n  const loopback = [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(url.hostname);\n  if ((url.protocol !== \"https:\" && !(url.protocol === \"http:\" && loopback)) ||\n      url.username || url.password || url.search || url.hash ||\n      ![\"\", \"/\", \"/v1\", \"/v1/\"].includes(url.pathname)) {\n    throw new Error(\"CreateOS API URL must be an HTTPS origin (optionally ending in /v1); HTTP is allowed on loopback only.\");\n  }\n  const shape = text(\"shape\");\n  if (!shape) throw new Error(\"CreateOS requires a shape from its shape catalog.\");\n  const timeoutMs = raw.timeoutMs ?? 300_000;","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/plugins/sandbox-providers/createos/src/config.ts#L1-L34","documentation":"parseConfig's text() helper validates each string config key: a present value must be a non-empty, non-whitespace string containing no NUL bytes. This error is thrown when a config key like apiUrl, apiKey, shape, rootfs, or region is provided but is not a usable string — for example a number, an empty string, whitespace, or a string containing \"\\0\".","triggerScenarios":"Setting a config value to \"\" or \"   \", passing a numeric/boolean value where a string is expected (e.g. region: 1), or supplying a value containing a NUL byte (often from decoding binary or corrupted secret storage).","commonSituations":"Environment variables interpolated to empty strings, secrets managers returning undefined coerced to \"\", YAML/JSON configs with wrong types, or pasted credentials containing invisible control characters.","solutions":["Inspect the raw config object and print the offending key's value and type.","Coerce known-numeric values with String(value) before calling parseConfig, or fix the config source to emit strings.","Trim or re-enter the value if it is empty/whitespace-only; provide the actual credential/URL.","Re-upload the secret if it contains NUL or control bytes — it was likely corrupted."],"exampleFix":"// before\nconst config = parseConfig({ apiUrl: process.env.CREATEOS_API_URL }); // undefined/empty env -> error\n// after: validate and coerce first\nconst apiUrl = (process.env.CREATEOS_API_URL ?? \"\").trim();\nif (!apiUrl) throw new Error(\"CREATEOS_API_URL is required\");\nconst config = parseConfig({ apiUrl });","handlingStrategy":"validation","validationCode":"function prevalidateConfig(raw) {\n  for (const key of [\"apiUrl\", \"apiKey\", \"shape\", \"rootfs\", \"region\"]) {\n    const v = raw[key];\n    if (v == null) continue;\n    if (typeof v !== \"string\" || !v.trim() || v.includes(\"\\0\"))\n      throw new Error(`${key} must be a non-empty string before calling parseConfig`);\n  }\n}","typeGuard":"function isNonEmptyString(v: unknown): v is string {\n  return typeof v === \"string\" && v.trim().length > 0 && !v.includes(\"\\0\");\n}","tryCatchPattern":"try {\n  config = parseConfig(raw);\n} catch (err) {\n  if (err.message.includes(\"must be a non-empty string\")) {\n    const key = err.message.split(\" \")[0];\n    throw new Error(`Configuration field '${key}' is invalid; check the board plugin settings and secret source.`);\n  }\n  throw err;\n}","preventionTips":["Validate config types at the boundary (env/JSON) before parseConfig.","Never coerce numeric/boolean config values implicitly; convert explicitly with String().","Re-enter secrets if they contain control characters — the source is likely corrupted.","Add a startup config validation step that reports all invalid keys at once."],"tags":["config","validation","type-mismatch"],"backgroundTag":"invalid-config-value","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}