{"record":{"id":"dd767cc6af6b9711","repo":"paperclipai/paperclip","slug":"organization-authorization-required","errorCode":"organization_authorization_required","errorMessage":"Organization authorization is required","messagePattern":"Organization authorization is required","errorType":"http","errorClass":"ToolGatewayHttpError","httpStatus":409,"severity":"error","filePath":"server/src/services/tool-gateway.ts","lineNumber":4423,"sourceCode":"        .limit(1);\n      return grant;\n    };\n    const findOrganizationGrant = async () => {\n      const [grant] = await db\n        .select()\n        .from(connectionGrants)\n        .where(\n          and(\n            eq(connectionGrants.companyId, connection.companyId),\n            eq(connectionGrants.connectionId, connection.id),\n            eq(connectionGrants.kind, \"organization\"),\n            eq(connectionGrants.isDefault, true),\n            eq(connectionGrants.status, \"active\"),\n          ),\n        )\n        .limit(1);\n      if (!grant) {\n        throw new ToolGatewayHttpError(\n          409,\n          \"Organization authorization is required\",\n          \"organization_authorization_required\",\n          {\n            connectionId: connection.id,\n          },\n        );\n      }\n      const members = await db\n        .select({ subjectId: connectionGrantMembers.subjectId })\n        .from(connectionGrantMembers)\n        .where(\n          and(\n            eq(connectionGrantMembers.companyId, connection.companyId),\n            eq(connectionGrantMembers.grantId, grant.id),\n            eq(connectionGrantMembers.subjectType, \"user\"),\n          ),\n        );","sourceCodeStart":4405,"sourceCodeEnd":4441,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/tool-gateway.ts#L4405-L4441","documentation":"This ToolGatewayHttpError (HTTP 409) is thrown by the tool gateway's grant resolution when a connection's credential policy requires an organization-level grant, but no active default organization grant (kind='organization', isDefault=true, status='active') exists in connection_grants for that connection. The gateway refuses to proceed because there is no consent boundary authorizing the shared credential. It is a setup/completeness problem, not a runtime failure.","triggerScenarios":"Calling a governed MCP tool through the tool gateway for a connection whose credential policy is 'organization' while the company has never completed (or has deactivated/removed) the default organization grant for that connection: e.g. the owner started OAuth connect but never finalized the org-wide authorization, or the grant row's status was flipped from 'active' to 'revoked'/'inactive'.","commonSituations":"An admin shared a connection at the organization level but the grant creation transaction never ran; a teammate removed or disabled the org grant while agents were still using the connection; a database restore/seeding dropped connection_grants rows so the connection exists but its grant does not.","solutions":["Re-authorize the connection at the organization level (complete the connection OAuth/install flow so an active default organization grant is created in connection_grants).","Inspect connection_grants for the connectionId: SELECT * FROM connection_grants WHERE connection_id = ... AND kind = 'organization'; confirm is_default=true and status='active', and re-activate or recreate the grant if it was revoked.","If the connection should be per-user instead, change the connection's credentialPolicy so grant resolution looks for a user grant rather than an organization grant.","Verify the connectionId in the failing tool call matches the connection the organization actually authorized (mis-scoped connection lookups surface as a missing grant)."],"exampleFix":"// before: grant row exists but inactive\nUPDATE connection_grants SET status = 'active' WHERE connection_id = $1 AND kind = 'organization';\n// after (preferred): re-run the connection authorization flow so the app inserts\n// { kind: 'organization', isDefault: true, status: 'active' } itself\nawait connectionService.authorizeForOrganization(connectionId, actorUserId);","handlingStrategy":"try-catch","validationCode":"const [orgGrant] = await db.select().from(connectionGrants).where(and(eq(connectionGrants.connectionId, connectionId), eq(connectionGrants.kind, 'organization'), eq(connectionGrants.isDefault, true), eq(connectionGrants.status, 'active'))).limit(1);\nif (!orgGrant) throw new Error(`Connection ${connectionId} has no active organization grant; authorize it first.`);","typeGuard":"function hasActiveOrgGrant(g: ConnectionGrant | undefined): g is ConnectionGrant {\n  return !!g && g.kind === \"organization\" && g.isDefault === true && g.status === \"active\";\n}","tryCatchPattern":"try {\n  await callGovernedTool(session, connectionId, toolName, args);\n} catch (e) {\n  if (e instanceof ToolGatewayHttpError && e.code === \"organization_authorization_required\") {\n    return { status: \"needs_authorization\", connectionId: e.details.connectionId };\n  }\n  throw e;\n}","preventionTips":["After creating any organization-scoped connection, assert in tests that a default active organization grant row exists before enabling tool calls.","Monitor for revoked/deactivated organization grants on connections that still have active sessions.","Prefer the app's authorize flow over manual inserts so grant kind/isDefault/status invariants hold.","Surface connection health/authorization state in the UI so agents are only pointed at fully authorized connections."],"tags":["authorization","mcp","tool-gateway","config"],"backgroundTag":"authentication-required","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"}