different-ai/openwork · error · ApiError

invalid_cloud_plugin

invalid_cloud_plugin

Error message

resolved cloud plugin is required

What it means

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).

Source

Thrown at apps/server/src/cloud-plugins.ts:171

  const memberships = value.memberships.flatMap((entry) => {
    if (!isRecord(entry) || typeof entry.configObjectId !== "string") return [];
    const configObject = normalizeConfigObject(entry.configObject);
    return [{ configObjectId: entry.configObjectId, ...(configObject ? { configObject } : {}) }];
  });
  return {
    plugin: {
      id: value.plugin.id,
      name: value.plugin.name,
      description: typeof value.plugin.description === "string" ? value.plugin.description : null,
      updatedAt: typeof value.plugin.updatedAt === "string" ? value.plugin.updatedAt : null,
    },
    memberships,
  };
}

export function readCloudPluginResolved(value: unknown): CloudPluginResolved {
  const resolved = normalizeCloudPluginResolved(value);
  if (!resolved) throw new ApiError(400, "invalid_cloud_plugin", "resolved cloud plugin is required");
  return resolved;
}

function extractSkillBodyMarkdown(skillText: string): string {
  const trimmed = skillText.trim();
  if (!trimmed.startsWith("---")) return trimmed;
  const rest = trimmed.slice(3);
  const end = rest.indexOf("\n---");
  if (end === -1) return trimmed;
  return rest.slice(end + 4).replace(/^\s*\n?/, "");
}

function slugifyConfigObjectName(title: string, fallback: string): string {
  let base = title
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Pass the exact `resolved` object returned by the plugin bundle resolution endpoint (plugin + memberships), not the raw manifest.
  2. Re-run the resolve step to get a fresh, correctly shaped CloudPluginResolved instead of caching old payloads.
  3. Inspect the payload against the CloudPluginResolved shape: ensure plugin (id, name) and memberships exist.
  4. Migrate or delete stale stored cloud plugin records written by an older schema.

Example fix

// before
installCloudPlugin(manifestJson)              // raw manifest
// after
const { resolved } = await resolveClaudePluginBundle(url);
installCloudPlugin(resolved);                 // normalized CloudPluginResolved
Defensive patterns

Strategy: type-guard

Validate before calling

function looksLikeResolved(v: unknown): boolean {
  const r = v as { plugin?: unknown; memberships?: unknown } | null;
  return !!r && typeof r.plugin === "object" && r.plugin !== null && Array.isArray(r.memberships);
}
if (!looksLikeResolved(payload)) throw new Error("pass the resolved bundle, not the raw manifest");

Type guard

function isCloudPluginResolved(v: unknown): v is CloudPluginResolved {
  const r = v as { plugin?: { id?: unknown; name?: unknown }; memberships?: unknown } | null;
  return !!r && typeof r.plugin === "object" && r.plugin !== null &&
    typeof r.plugin.id === "string" && typeof r.plugin.name === "string" &&
    Array.isArray(r.memberships);
}

Try / catch

try {
  const resolved = readCloudPluginResolved(payload);
} catch (e) {
  if (isApiError(e) && e.code === "invalid_cloud_plugin") {
    // re-resolve the plugin bundle and retry with the fresh resolved object
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/c1cdfcfc629c4bd4. Report an issue: GitHub.