{"record":{"id":"08ea86674bad159c","repo":"paperclipai/paperclip","slug":"unresolved-api-schema-value-ref","errorCode":null,"errorMessage":"Unresolved API schema: ${value.$ref}","messagePattern":"Unresolved API schema: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/runner-api-catalog.ts","lineNumber":65,"sourceCode":"  if (/\\/issues$/.test(path)) return method === \"GET\" ? [\"search_tasks\"] : [\"create_task\"];\n  if (/\\/issues\\/\\{[^}]+\\}$/.test(path)) return method === \"GET\" ? [\"get_task_context\"] : [\"set_dependencies\", \"finish_task\", \"block_task\", \"request_review\"];\n  if (/\\/agents$/.test(path) && method === \"GET\") return [\"list_agents\"];\n  if (/\\/agents\\/(me|\\{[^}]+\\})$/.test(path) && method === \"GET\") return [\"get_agent\"];\n  if (/\\/approvals$/.test(path) && method === \"GET\") return [\"list_approvals\"];\n  if (/\\/approvals\\/\\{[^}]+\\}/.test(path) && method === \"GET\") return [\"get_approval\", \"get_approval_context\"];\n  return [];\n}\n\n// Descriptions and schemas are documentation, never authorization. Routes remain\n// authoritative, including conditional role, company and resource checks.\nexport function buildRunnerApiCatalog(document: Json = buildOpenApiDocument()): RunnerApiOperation[] {\n  function dereference(value: any, seen = new Set<string>()): any {\n    if (Array.isArray(value)) return value.map((entry) => dereference(entry, seen));\n    if (!value || typeof value !== \"object\") return value;\n    if (typeof value.$ref === \"string\" && value.$ref.startsWith(\"#/\")) {\n      if (seen.has(value.$ref)) return { description: `Recursive schema: ${value.$ref}` };\n      const target = value.$ref.slice(2).split(\"/\").reduce((node: any, key: string) => node?.[key.replace(/~1/g, \"/\").replace(/~0/g, \"~\")], document);\n      if (!target) throw new Error(`Unresolved API schema: ${value.$ref}`);\n      return dereference(target, new Set([...seen, value.$ref]));\n    }\n    return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, dereference(entry, seen)]));\n  }\n  const result: RunnerApiOperation[] = [];\n  for (const [path, item] of Object.entries<Json>(document.paths)) {\n    for (const [verb, operation] of Object.entries<Json>(item)) {\n      if (!METHODS.has(verb)) continue;\n      const method = verb.toUpperCase();\n      const restriction = runnerApiRestriction(method, path);\n      const skillReference = runnerApiReference[`${method} ${path.replace(/\\{[^}]+\\}/g, \"{}\")}`];\n      const protocol = !path.startsWith(\"/api/\") || /\\/(oauth|auth|runtime-tools|mcp|ws)(\\/|$)/.test(path)\n        || /\\/(claude-login|login-sessions|start-authorization|finalize-oauth-access)(\\/|$)/.test(path)\n        || /event-stream|websocket/i.test(JSON.stringify(operation.responses));\n      result.push({\n        operationId: `${method} ${path}`, method, path,\n        summary: operation.summary ?? `${method} ${path}`,\n        description: operation.description ?? \"\",","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/native-runtime/runner-api-catalog.ts#L47-L83","documentation":"buildRunnerApiCatalog dereferences internal $ref pointers (\"#/...\") in the OpenAPI document. When a $ref's JSON-pointer path does not resolve to a node in the document (after ~0/~1 unescaping), dereference throws \"Unresolved API schema: <ref>\". This is a build-time integrity check on the shipped API schema, not a caller-input error.","triggerScenarios":"The OpenAPI document bundled into runner-api-catalog contains a $ref like \"#/components/schemas/Foo\" while components.schemas.Foo is missing, renamed, or the pointer contains unescaped ~ or / characters.","commonSituations":"Editing or regenerating the API spec and dropping a schema; hand-editing document.paths and referencing a schema that no longer exists; codegen output drift between spec versions; copy-pasting an operation with refs into a truncated document.","solutions":["Fix the OpenAPI document so every $ref target exists at the pointed path (re-add or rename the missing schema).","Regenerate the API catalog document from the authoritative spec source instead of hand-editing.","Check $ref strings for JSON-pointer escaping: \"~\" must be ~0 and \"/\" must be ~1 inside tokens.","Add/extend a catalog build test that dereferences all paths so the dangling ref is caught at build time."],"exampleFix":"// before (document)\n{ \"components\": { \"schemas\": { \"Issue\": { ... } } }, \"paths\": { \"/issues/{id}\": { \"$ref\": \"#/components/schemas/IssueRef\" } } }\n// after\n{ \"components\": { \"schemas\": { \"Issue\": { ... }, \"IssueRef\": { ... } } }, ... }","handlingStrategy":"try-catch","validationCode":"function assertRefsResolve(doc: any) {\n  const refs = JSON.stringify(doc).match(/\"\\$ref\":\\s*\"#([^\"]+)\"/g) ?? [];\n  for (const r of refs) {\n    const ptr = JSON.parse(\"{\" + r + \"}\").$ref.slice(1);\n    const node = ptr.split(\"/\").filter(Boolean).reduce((n: any, k: string) => n?.[k.replace(/~1/g, \"/\").replace(/~0/g, \"~\")], doc);\n    if (!node) throw new Error(`Dangling $ref: #${ptr}`);\n  }\n}\n// run over the OpenAPI document before feeding buildRunnerApiCatalog","typeGuard":null,"tryCatchPattern":"try {\n  const catalog = buildRunnerApiCatalog(document);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"Unresolved API schema:\")) {\n    // log the $ref from err.message and fix the spec / regenerate the document\n  } else throw err;\n}","preventionTips":["Always regenerate the bundled OpenAPI document from the spec source; never hand-edit refs.","Add a build/test step that dereferences every $ref in the catalog document.","Use a linter (e.g. Spectral no-unresolved-refs) on the spec in CI.","When moving schemas between files, update every referencing $ref in the same change."],"tags":["openapi","schema","catalog","native-runtime"],"backgroundTag":"schema-validation-failed","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}