{"record":{"id":"59488ddbff34fadf","repo":"paperclipai/paperclip","slug":"ambiguous-personal-grant","errorCode":"ambiguous_personal_grant","errorMessage":"More than one delegated personal authorization matches this autonomous run","messagePattern":"More than one delegated personal authorization matches this autonomous run","errorType":"http","errorClass":"ToolGatewayHttpError","httpStatus":409,"severity":"error","filePath":"server/src/services/tool-gateway.ts","lineNumber":3379,"sourceCode":"    // accepted from agent input, so a run carrying it uses that owner's grant\n    // directly. Delegation is reserved for genuinely ownerless unattended runs.\n    let userGrant = connection.credentialPolicy === \"shared\" ? undefined : await findUserGrant();\n    if (!userGrant && !actingUserId && autonomous && session.agentId && connection.credentialPolicy !== \"shared\") {\n      const delegated = await db.select({ grant: connectionGrants }).from(connectionGrantDelegations).innerJoin(\n        connectionGrants,\n        and(\n          eq(connectionGrants.id, connectionGrantDelegations.grantId),\n          eq(connectionGrants.companyId, connectionGrantDelegations.companyId),\n        ),\n      ).where(and(\n        eq(connectionGrantDelegations.companyId, connection.companyId),\n        eq(connectionGrantDelegations.agentId, session.agentId),\n        eq(connectionGrants.connectionId, connection.id),\n        eq(connectionGrants.kind, \"user\"),\n        eq(connectionGrants.status, \"active\"),\n      ));\n      if (delegated.length > 1) {\n        throw new ToolGatewayHttpError(409, \"More than one delegated personal authorization matches this autonomous run\", \"ambiguous_personal_grant\", {\n          connectionId: connection.id,\n          agentId: session.agentId,\n        });\n      }\n      userGrant = delegated[0]?.grant;\n      if (userGrant?.subjectUserId) {\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, userGrant.subjectUserId),\n          eq(companyMemberships.status, \"active\"),\n        )).limit(1);\n        if (!membership) {\n          throw new ToolGatewayHttpError(403, \"The delegated personal grant owner is not an active company member\", \"grant_owner_membership_inactive\", {\n            connectionId: connection.id,\n            grantId: userGrant.id,\n          });\n        }","sourceCodeStart":3361,"sourceCodeEnd":3397,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/tool-gateway.ts#L3361-L3397","documentation":"The tool gateway resolves a user-scope ('personal') credential grant for an autonomous run by querying connection grant delegations for the session's agent. When more than one active delegated personal grant matches the same connection and agent, the gateway cannot pick one deterministically and throws this 409 instead of guessing.","triggerScenarios":"An agent session with agentId set requests a tool bound to a connection with kind 'user' credential policy, and the DB contains >=2 rows in connection_grant_delegations joining to active user-kind connection_grants for that (connectionId, agentId) pair.","commonSituations":"Re-hiring or re-delegating the same personal connection to the same agent without revoking the previous delegation; a bug or manual DB edit duplicating delegations; company merges that copy grant rows.","solutions":["Query connectionGrantDelegations for the agentId/connectionId and revoke or delete all but one active delegation","Set the older duplicate delegation's status to revoked/inactive via the grants admin API or UI","Re-run the tool call once a single active delegated personal grant remains"],"exampleFix":"// before: duplicate delegations\nawait db.delete(connectionGrantDelegations).where(and(eq(connectionGrantDelegations.agentId, agentId), eq(connectionGrantDelegations.grantId, staleGrantId)));\n// after: keep exactly one active delegation\nconst keep = delegated[0];\nawait db.update(connectionGrantDelegations).set({ status: 'revoked' }).where(and(eq(connectionGrantDelegations.agentId, agentId), ne(connectionGrantDelegations.grantId, keep.grantId)));","handlingStrategy":"validation","validationCode":"const delegated = await db.select().from(connectionGrantDelegations).where(and(eq(connectionGrantDelegations.agentId, agentId), eq(connectionGrantDelegations.grantId, grantId)));\nif (delegated.filter(d => d.status === 'active').length > 1) throw new Error('De-duplicate delegations before running');","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Revoke existing delegations before re-delegating a connection to the same agent","Add a unique constraint or upsert on (agentId, connectionId) for active delegations","Audit delegations after company/agent reconfiguration"],"tags":["authorization","conflict","tool-gateway"],"backgroundTag":"conflicting-config-options","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"}