paperclipai/paperclip · error
Unresolved API schema: ${value.$ref}
Error message
Unresolved API schema: ${value.$ref} What it means
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.
Source
Thrown at server/src/services/native-runtime/runner-api-catalog.ts:65
if (/\/issues$/.test(path)) return method === "GET" ? ["search_tasks"] : ["create_task"];
if (/\/issues\/\{[^}]+\}$/.test(path)) return method === "GET" ? ["get_task_context"] : ["set_dependencies", "finish_task", "block_task", "request_review"];
if (/\/agents$/.test(path) && method === "GET") return ["list_agents"];
if (/\/agents\/(me|\{[^}]+\})$/.test(path) && method === "GET") return ["get_agent"];
if (/\/approvals$/.test(path) && method === "GET") return ["list_approvals"];
if (/\/approvals\/\{[^}]+\}/.test(path) && method === "GET") return ["get_approval", "get_approval_context"];
return [];
}
// Descriptions and schemas are documentation, never authorization. Routes remain
// authoritative, including conditional role, company and resource checks.
export function buildRunnerApiCatalog(document: Json = buildOpenApiDocument()): RunnerApiOperation[] {
function dereference(value: any, seen = new Set<string>()): any {
if (Array.isArray(value)) return value.map((entry) => dereference(entry, seen));
if (!value || typeof value !== "object") return value;
if (typeof value.$ref === "string" && value.$ref.startsWith("#/")) {
if (seen.has(value.$ref)) return { description: `Recursive schema: ${value.$ref}` };
const target = value.$ref.slice(2).split("/").reduce((node: any, key: string) => node?.[key.replace(/~1/g, "/").replace(/~0/g, "~")], document);
if (!target) throw new Error(`Unresolved API schema: ${value.$ref}`);
return dereference(target, new Set([...seen, value.$ref]));
}
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, dereference(entry, seen)]));
}
const result: RunnerApiOperation[] = [];
for (const [path, item] of Object.entries<Json>(document.paths)) {
for (const [verb, operation] of Object.entries<Json>(item)) {
if (!METHODS.has(verb)) continue;
const method = verb.toUpperCase();
const restriction = runnerApiRestriction(method, path);
const skillReference = runnerApiReference[`${method} ${path.replace(/\{[^}]+\}/g, "{}")}`];
const protocol = !path.startsWith("/api/") || /\/(oauth|auth|runtime-tools|mcp|ws)(\/|$)/.test(path)
|| /\/(claude-login|login-sessions|start-authorization|finalize-oauth-access)(\/|$)/.test(path)
|| /event-stream|websocket/i.test(JSON.stringify(operation.responses));
result.push({
operationId: `${method} ${path}`, method, path,
summary: operation.summary ?? `${method} ${path}`,
description: operation.description ?? "",View on GitHub (pinned to 01ad858492)
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.
Example fix
// before (document)
{ "components": { "schemas": { "Issue": { ... } } }, "paths": { "/issues/{id}": { "$ref": "#/components/schemas/IssueRef" } } }
// after
{ "components": { "schemas": { "Issue": { ... }, "IssueRef": { ... } } }, ... } Defensive patterns
Strategy: try-catch
Validate before calling
function assertRefsResolve(doc: any) {
const refs = JSON.stringify(doc).match(/"\$ref":\s*"#([^"]+)"/g) ?? [];
for (const r of refs) {
const ptr = JSON.parse("{" + r + "}").$ref.slice(1);
const node = ptr.split("/").filter(Boolean).reduce((n: any, k: string) => n?.[k.replace(/~1/g, "/").replace(/~0/g, "~")], doc);
if (!node) throw new Error(`Dangling $ref: #${ptr}`);
}
}
// run over the OpenAPI document before feeding buildRunnerApiCatalog Try / catch
try {
const catalog = buildRunnerApiCatalog(document);
} catch (err) {
if (err instanceof Error && err.message.startsWith("Unresolved API schema:")) {
// log the $ref from err.message and fix the spec / regenerate the document
} else throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Unknown config key ${warning.path}; did you mean ${warning.s
- Unknown config key ${warning.path}; did you mean ${warning.s
- Worktree seed manifest is missing source path diagnostics.
- Unknown config key ${warning.path}; did you mean ${warning.s
- ${label} has existing tables but no migration journal. Run m
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/08ea86674bad159c.
Report an issue: GitHub.