{"record":{"id":"22f9166d5c778b02","repo":"mastra-ai/mastra","slug":"factory-transitions-require-an-authenticated-bound","errorCode":null,"errorMessage":"Factory transitions require an authenticated bound agent tool call.","messagePattern":"Factory transitions require an authenticated bound agent tool call\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/rules/tools.ts","lineNumber":73,"sourceCode":"\n  return {\n    factory_transition_work_item: createTool({\n      id: 'factory_transition_work_item',\n      description: isTriage\n        ? 'Report the triage classification and request a governed stage transition for the Factory work item exactly bound to this thread. Only bugs may request Planning autonomously; closure outcomes may request a terminal stage. Feature requests and other non-bug classifications that remain open must stay in their current Intake or Triage stage for maintainer approval.'\n        : 'Request a governed stage transition for the Factory work item exactly bound to this thread. Use the current revision from the factory-phase signal and explain why the transition is appropriate.',\n      inputSchema: isTriage ? triageTransitionInputSchema : transitionInputSchema,\n      requireApproval: true,\n      execute: async ({ stage, expectedRevision, rationale, ...input }, execution) => {\n        const currentResolution = await resolveFactorySessionAddress({\n          requestContext: execution.requestContext,\n          storage: options.storage,\n          sessions: options.sessions,\n        });\n        const currentAddress = currentResolution?.address ?? null;\n        const toolCallId = execution.agent?.toolCallId;\n        if (!currentAddress || !toolCallId) {\n          throw new Error('Factory transitions require an authenticated bound agent tool call.');\n        }\n        const binding = await options.storage.findActiveRunBinding(currentAddress);\n        // Authority is the work item this session is bound to, not the individual\n        // binding row. Handing the next role its turn in an existing session\n        // rotates the binding, and tools built for the previous role stay live\n        // across that rotation; keying on row identity would strand the run that\n        // the rotation exists to start. Re-pointing a session at a different item\n        // is the hijack this guards against.\n        if (!binding || binding.workItemId !== availableBinding.workItemId) {\n          throw new Error('Factory agent binding is unavailable, revoked, or no longer matches this session.');\n        }\n        const item = await options.storage.get({ orgId: binding.orgId, id: binding.workItemId });\n        if (!item) throw new Error('Bound Factory work item not found.');\n        const triageType =\n          'triageType' in input && isFactoryTriageType(input.triageType) ? input.triageType : undefined;\n\n        const result = await options.transitionService.transition({\n          orgId: binding.orgId,","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/rules/tools.ts#L55-L91","documentation":"The `factory_transition_work_item` tool only executes for a genuine, authenticated bound-agent tool call. During execution it re-resolves the session address from request context and requires both a resolved session address and an `execution.agent.toolCallId`. If either is missing, the call is not a recognized agent tool invocation and cannot be attributed to a binding, so it is refused. This is an anti-hijack/authority gate: transitions must be attributable to a specific agent turn on a bound session.","triggerScenarios":"The tool's `execute` runs without `execution.agent.toolCallId` (e.g. invoked programmatically/outside an agent run), or `resolveFactorySessionAddress` returns null/undefined for `execution.requestContext` (no session address in request context), so `currentAddress` or `toolCallId` is null.","commonSituations":"Calling the tool directly from tests or scripts instead of through an agent run; running the agent without the factory request-context (factoryProjectId/session tags) populated; a framework upgrade changing the execution context shape so `execution.agent` is no longer populated.","solutions":["Invoke the tool only through a real agent run so the framework supplies `execution.agent.toolCallId`.","Ensure the session's factory request-context/tags (factoryProjectId, factoryOrgId, session address) are seeded before the run — see `session.state.set` in start-coordinator.","Pass the correct RequestContext (same one used to create the tools) into the agent execution so `resolveFactorySessionAddress` resolves.","Check @mastra/core version: if `execution.agent.toolCallId` moved, update the factory tools package to a compatible core version."],"exampleFix":"// before\ntool.execute({ stage: 'building', expectedRevision: 3, rationale: 'done' });\n// after\nconst run = await agent.start(prompt, { requestContext, resourceid: boundSessionId });\n// let the agent call the tool so execution.agent.toolCallId is present","handlingStrategy":"try-catch","validationCode":"const res = await resolveFactorySessionAddress({ requestContext, storage, sessions });\nif (!res?.address) throw new Error('No factory session address; transition tool will refuse.');","typeGuard":"function isBoundAgentExecution(\n  ex: unknown,\n): ex is { requestContext: RequestContext; agent: { toolCallId: string } } {\n  return !!ex && typeof ex === 'object' && 'agent' in ex &&\n    !!(ex as any).agent?.toolCallId && !!(ex as any).requestContext;\n}","tryCatchPattern":"try {\n  await transitionTool.execute(input, execution);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('authenticated bound agent tool call')) {\n    // surface to the model as a non-retryable authority error\n  } else throw e;\n}","preventionTips":["Never call factory transition tools outside an agent run; drive them through the agent loop only.","Seed session state (factoryProjectId, factoryOrgId, session address) server-side before the run, as start-coordinator does.","After upgrading @mastra/core, verify execution.agent.toolCallId is still populated in tool execute context.","Write a test asserting the tool rejects executions lacking toolCallId."],"tags":["factory","authentication","tool-execution","session-binding"],"backgroundTag":"unauthenticated-tool-invocation","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}