different-ai/openwork · error

Failed to verify skill plugin (${membershipResult.response.s

Error message

Failed to verify skill plugin (${membershipResult.response.status}).

What it means

Thrown inside useSkill when the companion request GET /v1/config-objects/{id}/plugins (used to determine whether the skill belongs to a plugin) returns a non-ok status, even though the main skill fetch succeeded. The user sees the error state even though the skill data itself is available, because both requests are awaited via Promise.all and either failure aborts the query.

Source

Thrown at ee/apps/den-web/app/(den)/dashboard/_components/skill-data.tsx:95

export function useSkill(pluginId: string, skillId: string) {
  const { orgId } = useOrgDashboard();
  const organizationId = orgId ?? "none";

  return useQuery({
    enabled: Boolean(orgId && pluginId && skillId),
    queryKey: skillQueryKeys.detail(organizationId, pluginId, skillId),
    queryFn: async (): Promise<DenSkill> => {
      const encodedSkillId = encodeURIComponent(skillId);
      const [{ response, payload }, membershipResult] = await Promise.all([
        requestJson(`/v1/config-objects/${encodedSkillId}`, { method: "GET" }, 15000),
        requestJson(`/v1/config-objects/${encodedSkillId}/plugins`, { method: "GET" }, 15000),
      ]);
      if (!response.ok) {
        throw new Error(getErrorMessage(payload, `Failed to load skill (${response.status}).`));
      }
      if (!membershipResult.response.ok) {
        throw new Error(getErrorMessage(membershipResult.payload, `Failed to verify skill plugin (${membershipResult.response.status}).`));
      }
      const belongsToPlugin = isRecord(membershipResult.payload)
        && Array.isArray(membershipResult.payload.items)
        && membershipResult.payload.items.some((entry) => (
          isRecord(entry) && entry.pluginId === pluginId && entry.removedAt === null
        ));
      if (!belongsToPlugin) {
        throw new Error("That skill is not part of this plugin.");
      }
      const skill = parseSkillResponse(payload);
      if (!skill) {
        throw new Error("Skill detail response was incomplete.");
      }
      return skill;
    },
  });
}

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Check the membership response status: 403 means permissions, 404 means route/version skew
  2. Grant the calling role permission to read config-object plugin membership, or fall back to a less privileged membership endpoint
  3. Deploy matching frontend and API versions so the /plugins route exists
  4. Treat membership lookup as non-fatal: default belongsToPlugin to false and show a warning instead of failing the whole skill query

Example fix

// before
if (!membershipResult.response.ok) {
  throw new Error(getErrorMessage(membershipResult.payload, `Failed to verify skill plugin (${membershipResult.response.status}).`));
}
// after
if (!membershipResult.response.ok) {
  console.warn(`Skill plugin membership check failed (${membershipResult.response.status}); assuming no plugin.`);
  return { skill: parseConfigObject(payload), belongsToPlugin: false };
}
Defensive patterns

Strategy: fallback

Validate before calling

// probe the membership endpoint non-fatally before composing the query
async function fetchMembership(skillId: string): Promise<Response | null> {
  const res = await fetch(`/v1/config-objects/${encodeURIComponent(skillId)}/plugins`);
  return res.ok ? res : null; // caller degrades gracefully when null
}

Type guard

function isMembershipList(payload: unknown): payload is { items: Array<{ pluginId: string; removedAt: string | null }> } {
  return typeof payload === "object" && payload !== null && Array.isArray((payload as { items?: unknown }).items);
}

Try / catch

try {
  const [{ response, payload }, membershipResult] = await Promise.all([
    requestJson(`/v1/config-objects/${encodedSkillId}`, { method: "GET" }, 15000),
    requestJson(`/v1/config-objects/${encodedSkillId}/plugins`, { method: "GET" }, 15000).catch(() => null),
  ]);
  if (!response.ok) throw new Error(getErrorMessage(payload, `Failed to load skill (${response.status}).`));
  const belongsToPlugin = membershipResult && membershipResult.response.ok && isMembershipList(membershipResult.payload)
    ? membershipResult.payload.items.some((e) => e.pluginId === pluginId && e.removedAt === null)
    : false;
} catch (error) {
  return { error: error instanceof Error ? error.message : "Failed to load skill." };
}

Prevention

When it happens

Trigger: GET /v1/config-objects/{encodedSkillId}/plugins responds 403 (endpoint not permitted for the user's role even though skill read is), 404 (route missing on an older API version), or 5xx — while the primary config-object GET is fine.

Common situations: Non-admin users lacking permission to list config-object plugin membership; API deployed without the /plugins sub-route (version skew); transient 5xx on the membership endpoint; wrong pluginId/org context in the request.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — 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/0af17056941f7ca0. Report an issue: GitHub.