{"record":{"id":"02e238930835cc95","repo":"lobehub/lobehub","slug":"forbidden-02e238","errorCode":"FORBIDDEN","errorMessage":"Agent not found or not editable","messagePattern":"Agent not found or not editable","errorType":"exception","errorClass":"TRPCError","httpStatus":403,"severity":"error","filePath":"apps/server/src/routers/lambda/composio.ts","lineNumber":203,"sourceCode":"  const existing = await connectorModel.findScopedByIdentifier(identifier, agentId);\n  if (existing) await connectorModel.delete(existing.id);\n}\n\n/**\n * Guard: the caller must OWN (have created) the agent before a Composio account\n * is bound to it. Uses `existsOwnedById` (creator-only) rather than the\n * visibility-aware `existsById`, so a member who can merely see a shared public\n * agent can't attach their account to it.\n */\nasync function assertCanEditAgent(\n  db: LobeChatDatabase,\n  userId: string,\n  agentId: string,\n  workspaceId?: string,\n): Promise<void> {\n  const agentModel = new AgentModel(db, userId, workspaceId);\n  if (!(await agentModel.existsOwnedById(agentId))) {\n    throw new TRPCError({ code: 'FORBIDDEN', message: 'Agent not found or not editable' });\n  }\n}\n\nexport const composioRouter = router({\n  createConnection: composioWriteProcedure\n    .input(\n      z.object({\n        /** Bind the connection to this agent (Agent > Personal). Requires edit rights. */\n        agentId: z.string().optional(),\n        appSlug: z.string(),\n        identifier: z.string(),\n        label: z.string(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const { appSlug, identifier, label, agentId } = input;\n      const { userId } = ctx;\n","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/lobehub/lobehub/blob/10f24d7ade75139093a9373b364f6bc91f3cd7db/apps/server/src/routers/lambda/composio.ts#L185-L221","documentation":"Thrown by `assertCanEditAgent` (used by `composio.createConnection` and `connector.create` when an `agentId` is supplied). It calls `AgentModel.existsOwnedById(agentId)` — a creator/owner check, not a visibility check — so it fires when the agent doesn't exist OR exists but the caller isn't its owner. The intent (per the doc comment) is to stop a member who can merely *see* a shared public agent from binding a credential to it.","triggerScenarios":"Calling `composio.createConnection({ agentId, ... })` or `connector.create({ agentId, ... })` where `agentId` belongs to another member or to no agent at all. A viewer fails here even on a public agent they can see, because `existsOwnedById` is creator-scoped.","commonSituations":"Member tries to attach a Composio/MCP connection to a team-shared agent they don't own; stale `agentId` from a deleted agent; cross-workspace id leak.","solutions":["Confirm the caller is the owner (or workspace Owner/Admin) of the target agent before binding a connection.","If sharing a connection is intended, the agent's owner should create it, or ownership should be transferred.","Verify the `agentId` is from the current workspace and still exists via `agent.existsOwnedById`.","On the client, hide the 'attach to agent' option for agents the user doesn't own."],"exampleFix":"// before\nawait trpc.composio.createConnection.mutate({ agentId, appSlug, identifier, label });\n\n// after — verify ownership in the UI first\nconst canEdit = agent.ownerId === currentUserId || hasRole(['Owner','Admin']);\nif (!canEdit) disableAttachButton();\nelse await trpc.composio.createConnection.mutate({ agentId, appSlug, identifier, label });","handlingStrategy":"validation","validationCode":"// Verify ownership in the UI before offering 'attach to agent'\nconst canEdit = agent.ownerId === currentUserId || workspaceRole === 'Owner' || workspaceRole === 'Admin';\nif (agentId && !canEdit) { disableAttachToAgent(); return; }","typeGuard":"const isForbidden = (e: unknown): boolean =>\n  typeof e === 'object' && e !== null && (e as any).data?.code === 'FORBIDDEN';","tryCatchPattern":"try {\n  await trpc.composio.createConnection.mutate({ agentId, appSlug, identifier, label });\n} catch (e) {\n  if (isForbidden(e)) { notifyNotAgentOwner(); return; }\n  throw e;\n}","preventionTips":["Only offer 'attach connection to agent' for agents the caller owns (or for Owner/Admin role).","Remember a viewer is blocked even on a public shared agent — visibility ≠ edit rights.","Use `AgentModel.existsOwnedById` semantics when gating UI: creator-only, not visibility."],"tags":["trpc","forbidden","composio","rbac","authorization","agent-ownership"],"backgroundTag":null,"analyzedSha":"10f24d7ade75139093a9373b364f6bc91f3cd7db","analyzedAt":"2026-08-12T11:43:19.543Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}