{"record":{"id":"c1cdfcfc629c4bd4","repo":"different-ai/openwork","slug":"invalid-cloud-plugin","errorCode":"invalid_cloud_plugin","errorMessage":"resolved cloud plugin is required","messagePattern":"resolved cloud plugin is required","errorType":"validation","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"apps/server/src/cloud-plugins.ts","lineNumber":171,"sourceCode":"  const memberships = value.memberships.flatMap((entry) => {\n    if (!isRecord(entry) || typeof entry.configObjectId !== \"string\") return [];\n    const configObject = normalizeConfigObject(entry.configObject);\n    return [{ configObjectId: entry.configObjectId, ...(configObject ? { configObject } : {}) }];\n  });\n  return {\n    plugin: {\n      id: value.plugin.id,\n      name: value.plugin.name,\n      description: typeof value.plugin.description === \"string\" ? value.plugin.description : null,\n      updatedAt: typeof value.plugin.updatedAt === \"string\" ? value.plugin.updatedAt : null,\n    },\n    memberships,\n  };\n}\n\nexport function readCloudPluginResolved(value: unknown): CloudPluginResolved {\n  const resolved = normalizeCloudPluginResolved(value);\n  if (!resolved) throw new ApiError(400, \"invalid_cloud_plugin\", \"resolved cloud plugin is required\");\n  return resolved;\n}\n\nfunction extractSkillBodyMarkdown(skillText: string): string {\n  const trimmed = skillText.trim();\n  if (!trimmed.startsWith(\"---\")) return trimmed;\n  const rest = trimmed.slice(3);\n  const end = rest.indexOf(\"\\n---\");\n  if (end === -1) return trimmed;\n  return rest.slice(end + 4).replace(/^\\s*\\n?/, \"\");\n}\n\nfunction slugifyConfigObjectName(title: string, fallback: string): string {\n  let base = title\n    .trim()\n    .toLowerCase()\n    .replace(/[^a-z0-9]+/g, \"-\")\n    .replace(/^-+|-+$/g, \"\");","sourceCodeStart":153,"sourceCodeEnd":189,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/cloud-plugins.ts#L153-L189","documentation":"readCloudPluginResolved normalizes its input via normalizeCloudPluginResolved and throws this 400 invalid_cloud_plugin ApiError when the value does not match the expected CloudPluginResolved shape (plugin metadata plus memberships). It guards the boundary where a previously resolved cloud plugin payload is consumed (e.g. from a request or stored data).","triggerScenarios":"Calling the API/function with a payload that is not a normalized resolved plugin: missing plugin object, missing memberships array, passing the raw manifest instead of the resolved bundle, or truncated/stale data.","commonSituations":"Client passing the manifest or the raw GitHub tree response instead of the `resolved` object returned by the bundle endpoint; a persisted cloud plugin record from an older schema version; JSON shape drift after an API update.","solutions":["Pass the exact `resolved` object returned by the plugin bundle resolution endpoint (plugin + memberships), not the raw manifest.","Re-run the resolve step to get a fresh, correctly shaped CloudPluginResolved instead of caching old payloads.","Inspect the payload against the CloudPluginResolved shape: ensure plugin (id, name) and memberships exist.","Migrate or delete stale stored cloud plugin records written by an older schema."],"exampleFix":"// before\ninstallCloudPlugin(manifestJson)              // raw manifest\n// after\nconst { resolved } = await resolveClaudePluginBundle(url);\ninstallCloudPlugin(resolved);                 // normalized CloudPluginResolved","handlingStrategy":"type-guard","validationCode":"function looksLikeResolved(v: unknown): boolean {\n  const r = v as { plugin?: unknown; memberships?: unknown } | null;\n  return !!r && typeof r.plugin === \"object\" && r.plugin !== null && Array.isArray(r.memberships);\n}\nif (!looksLikeResolved(payload)) throw new Error(\"pass the resolved bundle, not the raw manifest\");","typeGuard":"function isCloudPluginResolved(v: unknown): v is CloudPluginResolved {\n  const r = v as { plugin?: { id?: unknown; name?: unknown }; memberships?: unknown } | null;\n  return !!r && typeof r.plugin === \"object\" && r.plugin !== null &&\n    typeof r.plugin.id === \"string\" && typeof r.plugin.name === \"string\" &&\n    Array.isArray(r.memberships);\n}","tryCatchPattern":"try {\n  const resolved = readCloudPluginResolved(payload);\n} catch (e) {\n  if (isApiError(e) && e.code === \"invalid_cloud_plugin\") {\n    // re-resolve the plugin bundle and retry with the fresh resolved object\n  }\n}","preventionTips":["Always pass the `resolved` object from the bundle endpoint onward, never the raw manifest.","Re-resolve rather than caching resolved payloads across API versions.","Validate stored cloud plugin records after schema migrations.","Type the payload as CloudPluginResolved end-to-end so shape drift is caught at compile time."],"tags":["validation","schema","plugin-install","api"],"backgroundTag":"schema-validation-failed","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}