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.
Solutions
- Restore the grant owner's active company membership (or upgrade from viewer to member/admin)
- Re-authorize the GitHub grant under a different active member
- Delete/re-create the personal grant under an eligible member's identity
- 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
- Reassign or delete personal grants when the owner's membership changes
- Block owner deactivation/downgrade while grants they own are in use
- Audit user-kind grants against active memberships on a schedule
- Prefer org/shared grants over personal ones where possible
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
- Could not prepare managed GitHub launchers
- Discord command registration authorization denied
- github_identity_unavailable
- GitHub inventory failed: the active installation has not…
- grant_audience_denied
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/4986871907f9d5bd.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/tool-gateway.ts:4352
"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 })View on GitHub (pinned to 3f1d897a7c)