paperclipai/paperclip · error · ToolGatewayHttpError

grant_owner_membership_inactive

grant_owner_membership_inactive

Error message

The personal grant owner is not an authorized company member

What it means

Thrown when a personal (user-kind) GitHub grant is selected but its owner is not an active company member, or their membership role is 'viewer'. Viewers and non-members may not lend their personal GitHub identity to company tool calls, so a 403 is raised.

Source

Thrown at server/src/services/tool-gateway.ts:3267

    if (session.identityContextId && session.agentId && (
      connection.config.sourceTemplateKey === "github" || connection.transportConfig?.sourceTemplateKey === "github"
    )) {
      const captured = githubOperationCredentials.get(session);
      const selected = captured
        ? { grant: captured.grant, error: undefined }
        : await resolveManagedGitHubIdentitySelection(db, session.companyId, {
          agentId: session.agentId, responsibleUserId: session.responsibleUserId, allowStandingDelegation: false,
        });
      if (!selected.grant || selected.grant.connectionId !== connection.id) {
        throw new ToolGatewayHttpError(409, selected.error ?? "GitHub identity changed; retry through the managed tool", "github_identity_unavailable");
      }
      if (selected.grant.kind === "user") {
        const [member] = await db.select({ role: companyMemberships.membershipRole }).from(companyMemberships).where(and(
          eq(companyMemberships.companyId, session.companyId), eq(companyMemberships.principalType, "user"),
          eq(companyMemberships.principalId, selected.grant.subjectUserId!), eq(companyMemberships.status, "active"),
        )).limit(1);
        if (!member || member.role === "viewer") {
          throw new ToolGatewayHttpError(403, "The personal grant owner is not an authorized company member", "grant_owner_membership_inactive");
        }
      }
      // Managed GitHub selection is final: a legacy shared policy cannot replace
      // the captured person's grant with an organization or teammate's account.
      return selected.grant;
    }
    const autonomous = !session.identityContextId && (run?.invocationSource === "automation" || run?.invocationSource === "timer");
    const findUserGrant = async () => {
      if (!actingUserId) return undefined;
      const [membership] = await db.select({ id: companyMemberships.id }).from(companyMemberships).where(and(
        eq(companyMemberships.companyId, connection.companyId),
        eq(companyMemberships.principalType, "user"),
        eq(companyMemberships.principalId, actingUserId),
        eq(companyMemberships.status, "active"),
      )).limit(1);
      if (!membership) {
        throw new ToolGatewayHttpError(403, "The personal grant owner is not an active company member", "grant_owner_membership_inactive", {
          connectionId: connection.id,

View on GitHub (pinned to 01ad858492)

Solutions

  1. Restore the grant owner's active company membership (or upgrade from viewer to member/admin)
  2. Re-authorize the GitHub grant under a different active member
  3. Delete/re-create the personal grant under an eligible member's identity
  4. Audit grants whose owners have left and clean them up

Example fix

// before
// grant owner role: viewer -> 403
await callGitHubTool(session, connId, p);
// after
await db.update(companyMemberships).set({ membershipRole: "member" })
  .where(eq(companyMemberships.principalId, grantOwnerId));
await callGitHubTool(session, connId, p);
Defensive patterns

Strategy: validation

Validate before calling

const [m] = await db.select().from(companyMemberships).where(and(
  eq(companyMemberships.companyId, companyId),
  eq(companyMemberships.principalId, grantOwnerUserId),
  eq(companyMemberships.status, "active")));
if (!m || m.membershipRole === "viewer") throw new Error("Grant owner not eligible; reassign grant");

Type guard

function ownerIsEligibleMember(m?: { membershipRole: string } | null): boolean {
  return !!m && m.membershipRole !== "viewer";
}

Try / catch

try { await callGitHubTool(session, connId, p); }
catch (e) {
  if (e.code === "grant_owner_membership_inactive") await reassignGrantToActiveMember(connId);
  else throw e;
}

Prevention

When it happens

Trigger: Managed GitHub identity resolution selects a user-kind grant whose subjectUserId has no active companyMemberships row for the session's company, or the row has membershipRole='viewer'.

Common situations: Grant owner left the company or their membership was deactivated after authorizing the GitHub grant; owner downgraded to viewer role; membership row deleted while the grant persisted; agent work reassigned to a viewer.

Understand the failure class

Background: "You do not have permission" / 403 Forbidden errors: authenticated but not allowed — causes and fixes across open-source libraries — this error's family across 31 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/4986871907f9d5bd. Report an issue: GitHub.