{"record":{"id":"7d5c879d0fb7675c","repo":"mastra-ai/mastra","slug":"access-denied-durable-run-belongs-to-a-different","errorCode":null,"errorMessage":"Access denied: durable run belongs to a different resource","messagePattern":"Access denied: durable run belongs to a different resource","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"packages/server/src/server/handlers/agents.ts","lineNumber":217,"sourceCode":"  runId,\n  toolCallId,\n  requestContext,\n}: {\n  mastra: any;\n  agent: Agent;\n  runId: string;\n  toolCallId: string;\n  requestContext: RequestContext;\n}): Promise<void> {\n  if (!isDurableAgentLike(agent)) return;\n\n  const workflowsStore = await mastra.getStorage()?.getStore('workflows');\n  const workflowRun = await workflowsStore?.getWorkflowRunById({\n    workflowName: DurableStepIds.AGENTIC_LOOP,\n    runId,\n  });\n  if (!workflowRun) {\n    throw new HTTPException(403, { message: 'Access denied: durable run belongs to a different resource' });\n  }\n\n  let snapshot = workflowRun.snapshot as Record<string, any> | string | undefined;\n  if (typeof snapshot === 'string') {\n    try {\n      snapshot = JSON.parse(snapshot) as Record<string, any>;\n    } catch {\n      snapshot = undefined;\n    }\n  }\n\n  const input = snapshot?.context?.input;\n  const persistedResourceIds = new Set(\n    [\n      workflowRun.resourceId,\n      input?.state?.resourceId,\n      input?.messageListState?.memoryInfo?.resourceId,\n      input?.requestContextEntries?.[MASTRA_RESOURCE_ID_KEY],","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/server/src/server/handlers/agents.ts#L199-L235","documentation":"Thrown by validateDurableToolCallAccess (used by the approve/decline tool-call routes, both generate and stream variants) when no durable workflow run exists for the given runId under the AGENTIC_LOOP workflow. The 403 intentionally hides whether the run exists, tying the run to the caller's resource scope.","triggerScenarios":"POSTing to the durable tool approve/decline endpoints with a runId that was never persisted, was already completed and pruned, or belongs to another resource; storage not returning the run because getWorkflowRunById is scoped by workflowName DurableStepIds.AGENTIC_LOOP and the run was created under a different workflow.","commonSituations":"Approving a tool call after the run finished and its snapshot was cleaned up; sharing a runId between users/resources (multi-tenant setups); pointing at the wrong Mastra server/environment where the runId does not exist; calling approve twice after the run transitioned out of suspension.","solutions":["Confirm the runId comes from a currently-suspended durable agent run in the same storage backend/environment","Re-check that the caller's resourceId matches the resource that started the run — approve/decline from the originating resource's context","Handle the 403 by refreshing run state from the agent's tool-call list instead of retrying the stale runId"],"exampleFix":"// before\nawait client.approveToolCall({ runId: staleRunId, toolCallId })\n// after\nconst pending = await client.getPendingToolCalls({ resourceId });\nif (pending.some(c => c.runId === runId && c.toolCallId === toolCallId)) {\n  await client.approveToolCall({ runId, toolCallId });\n}","handlingStrategy":"try-catch","validationCode":"// client-side: only act on runIds returned by the durable agent run in this environment\nconst pending = await getPendingToolCalls({ resourceId: myResourceId });\nif (!pending.some(c => c.runId === runId)) return skip();","typeGuard":"function isRunnableRun(run: { status: string } | null | undefined): run is { status: 'suspended' } {\n  return !!run && run.status === 'suspended';\n}","tryCatchPattern":"try {\n  await client.approveToolCall({ runId, toolCallId });\n} catch (e) {\n  if (isHttpException(e, 403) && String(e.message).includes('durable run')) {\n    // run unknown to this storage/resource: refresh pending list, do not retry\n  }\n}","preventionTips":["Only use runIds obtained from the same server/storage instance","Treat approve/decline as one-shot actions; disable controls after the first call","In multi-tenant apps, never accept runIds from cross-user input without re-authorization","Poll run status before acting to confirm the run is still suspended"],"tags":["http-403","authorization","durable-workflow","multi-tenancy"],"backgroundTag":"durable-run-access-denied","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}