{"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":3267,"sourceCode":"    if (session.identityContextId && session.agentId && (\n      connection.config.sourceTemplateKey === \"github\" || connection.transportConfig?.sourceTemplateKey === \"github\"\n    )) {\n      const captured = githubOperationCredentials.get(session);\n      const selected = captured\n        ? { grant: captured.grant, error: undefined }\n        : await resolveManagedGitHubIdentitySelection(db, session.companyId, {\n          agentId: session.agentId, responsibleUserId: session.responsibleUserId, allowStandingDelegation: false,\n        });\n      if (!selected.grant || selected.grant.connectionId !== connection.id) {\n        throw new ToolGatewayHttpError(409, selected.error ?? \"GitHub identity changed; retry through the managed tool\", \"github_identity_unavailable\");\n      }\n      if (selected.grant.kind === \"user\") {\n        const [member] = await db.select({ role: companyMemberships.membershipRole }).from(companyMemberships).where(and(\n          eq(companyMemberships.companyId, session.companyId), eq(companyMemberships.principalType, \"user\"),\n          eq(companyMemberships.principalId, selected.grant.subjectUserId!), eq(companyMemberships.status, \"active\"),\n        )).limit(1);\n        if (!member || member.role === \"viewer\") {\n          throw new ToolGatewayHttpError(403, \"The personal grant owner is not an authorized company member\", \"grant_owner_membership_inactive\");\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 = !session.identityContextId && (run?.invocationSource === \"automation\" || run?.invocationSource === \"timer\");\n    const findUserGrant = async () => {\n      if (!actingUserId) return undefined;\n      const [membership] = await db.select({ id: companyMemberships.id }).from(companyMemberships).where(and(\n        eq(companyMemberships.companyId, connection.companyId),\n        eq(companyMemberships.principalType, \"user\"),\n        eq(companyMemberships.principalId, actingUserId),\n        eq(companyMemberships.status, \"active\"),\n      )).limit(1);\n      if (!membership) {\n        throw new ToolGatewayHttpError(403, \"The personal grant owner is not an active company member\", \"grant_owner_membership_inactive\", {\n          connectionId: connection.id,","sourceCodeStart":3249,"sourceCodeEnd":3285,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/tool-gateway.ts#L3249-L3285","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":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}