{"record":{"id":"fcc646a6aef3de8d","repo":"paperclipai/paperclip","slug":"grant-audience-denied","errorCode":"grant_audience_denied","errorMessage":"The acting user is not in this grant's audience","messagePattern":"The acting user is not in this grant's audience","errorType":"http","errorClass":"ToolGatewayHttpError","httpStatus":403,"severity":"error","filePath":"server/src/services/tool-gateway.ts","lineNumber":4464,"sourceCode":"            .where(\n              and(\n                eq(companyMemberships.companyId, connection.companyId),\n                eq(companyMemberships.principalType, \"user\"),\n                eq(companyMemberships.principalId, actingUserId),\n                eq(companyMemberships.status, \"active\"),\n              ),\n            )\n            .limit(1)\n            .then((rows) => rows[0] ?? null)\n        : null;\n      if (\n        !isConnectionGrantAudienceAllowed(\n          members.map((member) => member.subjectId),\n          actingUserId,\n          Boolean(activeAudienceMember),\n        )\n      ) {\n        throw new ToolGatewayHttpError(\n          403,\n          \"The acting user is not in this grant's audience\",\n          \"grant_audience_denied\",\n          {\n            connectionId: connection.id,\n            grantId: grant.id,\n            actingUserId,\n          },\n        );\n      }\n      return grant;\n    };\n\n    if (connection.credentialPolicy === \"per_agent\") {\n      if (!session.agentId) {\n        throw new ToolGatewayHttpError(\n          409,\n          \"A dedicated agent authorization is required\",","sourceCodeStart":4446,"sourceCodeEnd":4482,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/tool-gateway.ts#L4446-L4482","documentation":"This ToolGatewayHttpError (HTTP 403) is thrown when an active organization grant exists for a connection, but the acting user is not part of that grant's audience: isConnectionGrantAudienceAllowed(members, actingUserId, isActiveMember) returned false. The audience is the list of user subjects in connection_grant_members (subjectType='user'); an empty audience only permits users who are not active company members, so a real user absent from the list is denied. It enforces that shared credentials can only be used by explicitly admitted users.","triggerScenarios":"A user (actingUserId) invokes a governed tool for an organization-granted connection while (a) their userId is not present in connection_grant_members for that grant, or (b) they are not an active company member and the grant audience list does not include them — either way isConnectionGrantAudienceAllowed returns false.","commonSituations":"A new teammate joins the company after the org grant was created and is never added to the grant audience; an admin intentionally restricts the grant audience and a non-listed user's agent triggers the call; the acting user's company membership lapsed (status not 'active') so they no longer count as the active-member escape hatch.","solutions":["Add the acting user to the grant's audience by inserting a connection_grant_members row {companyId, grantId, subjectType:'user', subjectId: actingUserId}, or re-save the grant with the user selected in the audience picker.","Restore the user's active company membership (set company_memberships.status='active') so the active-member branch of the audience check passes.","If the grant audience was intentionally narrowed, have a listed user (or an agent running under a listed owner's responsibleUserId) make the call instead.","Confirm the actingUserId being resolved by the session/run matches the intended user — stale or wrong identity context will not match the audience list."],"exampleFix":"// before: user missing from audience\nawait db.insert(connectionGrantMembers).values({\n  companyId: connection.companyId,\n  grantId: grant.id,\n  subjectType: \"user\",\n  subjectId: actingUserId,\n});\n// after: user admitted to the org grant audience","handlingStrategy":"validation","validationCode":"const [isMember] = await db.select({ id: companyMemberships.id }).from(companyMemberships).where(and(eq(companyMemberships.companyId, companyId), eq(companyMemberships.principalType, 'user'), eq(companyMemberships.principalId, actingUserId), eq(companyMemberships.status, 'active'))).limit(1);\nconst audience = await db.select({ subjectId: connectionGrantMembers.subjectId }).from(connectionGrantMembers).where(and(eq(connectionGrantMembers.grantId, grantId), eq(connectionGrantMembers.subjectType, 'user')));\nconst allowed = audience.length === 0 ? !isMember : audience.some(m => m.subjectId === actingUserId);\nif (!allowed) throw new Error(`User ${actingUserId} is not in grant ${grantId} audience.`);","typeGuard":"function isInGrantAudience(audience: { subjectId: string }[], actingUserId: string | null, isActiveMember: boolean): boolean {\n  return audience.some((m) => m.subjectId === actingUserId) || audience.length === 0 && isActiveMember;\n}","tryCatchPattern":"try {\n  await callGovernedTool(session, connectionId, toolName, args);\n} catch (e) {\n  if (e instanceof ToolGatewayHttpError && e.code === \"grant_audience_denied\") {\n    return { status: \"audience_denied\", grantId: e.details.grantId, remediation: \"add user to grant audience or restore membership\" };\n  }\n  throw e;\n}","preventionTips":["When onboarding new company members, add them to the audiences of the org grants they need (or keep audience lists in sync with membership automatically).","Keep user memberships active; treat lapsed membership as a first suspect when tool calls start failing.","Run the audience check (mirrored above) in CI or a preflight step for agent run configurations that name a connection.","Avoid empty audiences on org grants unless 'non-members only' is the intended, documented semantics."],"tags":["authorization","permissions","mcp","tool-gateway"],"backgroundTag":"insufficient-permissions","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}