{"record":{"id":"4986871907f9d5bd","repo":"paperclipai/paperclip","slug":"grant-owner-membership-inactive","errorCode":"grant_owner_membership_inactive","errorMessage":"The personal grant owner is not an authorized company member","messagePattern":"The personal grant owner is not an authorized company member","errorType":"http","errorClass":"ToolGatewayHttpError","httpStatus":403,"severity":"error","filePath":"server/src/services/tool-gateway.ts","lineNumber":4352,"sourceCode":"            \"GitHub identity changed; retry through the managed tool\",\n          \"github_identity_unavailable\",\n        );\n      }\n      if (selected.grant.kind === \"user\") {\n        const [member] = await db\n          .select({ role: companyMemberships.membershipRole })\n          .from(companyMemberships)\n          .where(\n            and(\n              eq(companyMemberships.companyId, session.companyId),\n              eq(companyMemberships.principalType, \"user\"),\n              eq(companyMemberships.principalId, selected.grant.subjectUserId!),\n              eq(companyMemberships.status, \"active\"),\n            ),\n          )\n          .limit(1);\n        if (!member || member.role === \"viewer\") {\n          throw new ToolGatewayHttpError(\n            403,\n            \"The personal grant owner is not an authorized company member\",\n            \"grant_owner_membership_inactive\",\n          );\n        }\n      }\n      // Managed GitHub selection is final: a legacy shared policy cannot replace\n      // the captured person's grant with an organization or teammate's account.\n      return selected.grant;\n    }\n    const autonomous =\n      !session.identityContextId &&\n      (run?.invocationSource === \"automation\" ||\n        run?.invocationSource === \"timer\");\n    const findUserGrant = async () => {\n      if (!actingUserId) return undefined;\n      const [membership] = await db\n        .select({ id: companyMemberships.id })","sourceCodeStart":4334,"sourceCodeEnd":4370,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/tool-gateway.ts#L4334-L4370","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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.","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"],"exampleFix":"// before\n// grant owner role: viewer -> 403\nawait callGitHubTool(session, connId, p);\n// after\nawait db.update(companyMemberships).set({ membershipRole: \"member\" })\n  .where(eq(companyMemberships.principalId, grantOwnerId));\nawait callGitHubTool(session, connId, p);","handlingStrategy":"validation","validationCode":"const [m] = await db.select().from(companyMemberships).where(and(\n  eq(companyMemberships.companyId, companyId),\n  eq(companyMemberships.principalId, grantOwnerUserId),\n  eq(companyMemberships.status, \"active\")));\nif (!m || m.membershipRole === \"viewer\") throw new Error(\"Grant owner not eligible; reassign grant\");","typeGuard":"function ownerIsEligibleMember(m?: { membershipRole: string } | null): boolean {\n  return !!m && m.membershipRole !== \"viewer\";\n}","tryCatchPattern":"try { await callGitHubTool(session, connId, p); }\ncatch (e) {\n  if (e.code === \"grant_owner_membership_inactive\") await reassignGrantToActiveMember(connId);\n  else throw e;\n}","preventionTips":["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"],"tags":["github","permissions","membership","authorization"],"backgroundTag":"insufficient-permissions","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}