{"record":{"id":"bbba597ab1a1da69","repo":"mastra-ai/mastra","slug":"access-denied-agent-has-no-memory-configured-to-v","errorCode":null,"errorMessage":"Access denied: agent has no memory configured to validate thread ownership","messagePattern":"Access denied: agent has no memory configured to validate thread ownership","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"packages/server/src/server/handlers/agents.ts","lineNumber":2678,"sourceCode":"        mastra,\n        agentId,\n        versionOptions: extractVersionOptions(requestContext),\n      });\n\n      // Honor server-enforced thread/resource scoping from the request context\n      // so clients cannot list suspended runs outside their own scope.\n      const effectiveResourceId = getEffectiveResourceId(requestContext, query.resourceId);\n      const effectiveThreadId = getEffectiveThreadId(requestContext, query.threadId);\n\n      // Validate ownership/FGA before honoring a thread filter — without this a\n      // caller could probe another user's suspended approvals (including\n      // tool-call args) by guessing a threadId. Reject when ownership cannot be\n      // verified (no memory configured, or the thread does not exist) so a\n      // thread-scoped query is never honored unchecked.\n      if (effectiveThreadId) {\n        const memory = await agent.getMemory({ requestContext });\n        if (!memory) {\n          throw new HTTPException(403, {\n            message: 'Access denied: agent has no memory configured to validate thread ownership',\n          });\n        }\n        const thread = await memory.getThreadById({ threadId: effectiveThreadId });\n        if (!thread) {\n          throw new HTTPException(403, { message: 'Access denied: thread not found' });\n        }\n        await enforceThreadAccess({\n          mastra,\n          requestContext,\n          threadId: effectiveThreadId,\n          thread,\n          effectiveResourceId,\n        });\n      }\n\n      return await agent.listSuspendedRuns({\n        threadId: effectiveThreadId,","sourceCodeStart":2660,"sourceCodeEnd":2696,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/server/src/server/handlers/agents.ts#L2660-L2696","documentation":"GET /agents/:agentId/suspended-runs enforces thread-scoped access: when a threadId filter is present, the server must verify the caller owns that thread before honoring the query. If the agent has no memory configured there is no storage to look the thread up in, so ownership cannot be verified and the server fails closed with HTTPException 403 rather than returning another user's suspended approvals.","triggerScenarios":"Calling /agents/:agentId/suspended-runs?threadId=<id> (or with threadId supplied via request context) on an agent constructed without memory (no storage/libsql/mastra memory). Any thread-scoped listing on a memory-less agent triggers this.","commonSituations":"Agent defined without a memory block while the UI still passes threadId filters; storage not attached in a serverless/edge deployment; resource-scoped deployments where threadId comes from auth context automatically; confusion after removing memory from an agent config.","solutions":["Configure memory on the agent (attach a Memory instance with storage) so thread ownership can be validated","Drop the threadId filter and list suspended runs agent-wide, relying on resourceId scoping instead","Use a different agent that has memory configured","If the flow truly needs no threads, stop sending threadId in query/context"],"exampleFix":"// before\nexport const agent = new Agent({ name: 'helper', instructions: '...' }); // no memory\n// after\nexport const agent = new Agent({ name: 'helper', instructions: '...', memory: new Memory({ storage: new LibSQLStore({ url: process.env.DB_URL! }) }) });","handlingStrategy":"validation","validationCode":"const memory = agent.getMemory ? await agent.getMemory({}) : null;\nif (threadId && !memory) throw new Error('threadId filter requires an agent with memory configured');","typeGuard":"function canValidateThreads(a: unknown): a is { getMemory: () => Promise<unknown> } {\n  return typeof a === 'object' && a !== null && typeof (a as any).getMemory === 'function';\n}","tryCatchPattern":"try { await client.getAgent(agentId).listSuspendedRuns({ threadId }); } catch (e) { if (e?.status === 403 && /no memory configured/.test(e.message)) { return client.getAgent(agentId).listSuspendedRuns({ resourceId }); } throw e; }","preventionTips":["Attach memory (with storage) to any agent whose runs use threads or tool approvals","Do not pass threadId filters to memory-less agents","Document the memory requirement wherever suspended-run listing is used"],"tags":["http-403","access-denied","memory","security","thread-ownership"],"backgroundTag":"access-denied-thread-ownership","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}