{"record":{"id":"e5795a4f86f1483a","repo":"mastra-ai/mastra","slug":"access-denied-thread-belongs-to-a-different-resou","errorCode":null,"errorMessage":"Access denied: thread belongs to a different resource","messagePattern":"Access denied: thread belongs to a different resource","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"packages/server/src/server/handlers/utils.ts","lineNumber":121,"sourceCode":"export function getEffectiveThreadId(\n  requestContext: RequestContext | undefined,\n  clientThreadId: string | undefined,\n): string | undefined {\n  const contextThreadId = requestContext?.get(MASTRA_THREAD_ID_KEY) as string | undefined;\n  return contextThreadId || clientThreadId;\n}\n\n/**\n * Validates that a thread belongs to the specified resourceId.\n * Throws 403 if the thread exists but belongs to a different resource.\n * Threads with no resourceId are accessible to all (shared threads).\n */\nexport async function validateThreadOwnership(\n  thread: { resourceId?: string | null } | null | undefined,\n  effectiveResourceId: string | undefined,\n): Promise<void> {\n  if (thread && effectiveResourceId && thread.resourceId && thread.resourceId !== effectiveResourceId) {\n    throw new HTTPException(403, { message: 'Access denied: thread belongs to a different resource' });\n  }\n}\n\n/**\n * Validates both coarse resource ownership and fine-grained thread access.\n * FGA enforcement is a no-op when no FGA provider is configured.\n */\nexport async function enforceThreadAccess({\n  mastra,\n  requestContext,\n  threadId,\n  thread,\n  effectiveResourceId,\n  permission = MastraFGAPermissions.MEMORY_READ,\n}: {\n  mastra: any;\n  requestContext?: RequestContext;\n  threadId: string;","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/server/src/server/handlers/utils.ts#L103-L139","documentation":"validateThreadOwnership throws this 403 when a thread's resourceId does not match the effective resource ID derived from the request (body memory.resource or authenticated user). It prevents a user from reading, signaling, subscribing to, or aborting another user's conversation thread.","triggerScenarios":"Calling SEND_AGENT_SIGNAL, agent message, abort-thread, subscribe-thread, or stream-until-idle routes with a threadId whose stored resourceId differs from the caller's effectiveResourceId.","commonSituations":"Reusing a threadId from another user/test account; switching memory.resource between requests on the same thread; multiple devs sharing a dev server with different resource IDs; seeded threads with stale resourceId values after changing the resource mapping.","solutions":["Use the same memory.resource (or authenticated identity) that originally created the thread.","Create a new thread scoped to the current resource instead of reusing a foreign threadId.","Check storage for the thread's resourceId and align your requests with it.","Fix mapUserToResourceId or body memory.resource inconsistencies across environments."],"exampleFix":"// before\nconst res = await fetch(`/api/agents/assistant/thread/${threadId}/signal`, { method: 'POST', body: JSON.stringify({ memory: { resource: 'user-b' }, ... }) }); // thread belongs to user-a\n// after\nconst res = await fetch(`/api/agents/assistant/thread/${threadId}/signal`, { method: 'POST', body: JSON.stringify({ memory: { resource: 'user-a' }, ... }) }); // or use a thread created for user-b","handlingStrategy":"try-catch","validationCode":"async function assertThreadOwnable(threadId: string, resourceId: string, fetchThread: (id: string) => Promise<{ resourceId?: string | null } | null>) {\n  const thread = await fetchThread(threadId);\n  if (thread?.resourceId && thread.resourceId !== resourceId) {\n    throw new Error(`Thread ${threadId} belongs to resource '${thread.resourceId}', not '${resourceId}'`);\n  }\n}","typeGuard":"function threadBelongsToResource(thread: { resourceId?: string | null } | null, resourceId: string): boolean {\n  return !!thread && (!thread.resourceId || thread.resourceId === resourceId);\n}","tryCatchPattern":"try {\n  const res = await fetch(`/api/agents/assistant/thread/${threadId}/signal`, { method: 'POST', body: JSON.stringify(payload) });\n  if (res.status === 403) throw new Error(`Thread ${threadId} is owned by a different resource; use a thread created for this resource`);\n  return await res.json();\n} catch (e) { throw e; }","preventionTips":["Persist the (resourceId, threadId) pair together and always send the matching memory.resource.","Never share threadIds across users or environments.","Verify thread resourceId in storage when debugging access issues.","Keep mapUserToResourceId deterministic across deploys."],"tags":["http-403","authorization","memory","multi-tenancy"],"backgroundTag":"resource-ownership-mismatch","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}