Title: [BUG] Global ToolManager can leak state across concurrent workflows
Describe the bug
Paper2Any's workflow layer uses a process-wide singleton ToolManager, but GenericGraphBuilder._register_tools_for_role(...) registers pre-tools as lambdas that capture the current workflow state object.
Because those registrations are keyed only by (role, tool_name), one workflow can overwrite another workflow's state-bound pre-tool when they use the same role/tool pair.
That means overlapping workflows can execute with the wrong request state. In the reproduction below, workflow A registers paper_content = DOC_A, workflow B registers paper_content = DOC_B, and workflow A later executes against DOC_B.
To Reproduce
- From the repository root, save the following as
probe_tool_manager_leak.py.
- Run
python probe_tool_manager_leak.py.
- Observe that workflow A reads workflow B's
paper_content.
import asyncio
import sys
import types
def ensure_mod(name):
mod = types.ModuleType(name)
sys.modules[name] = mod
return mod
# Minimal stubs for import-time optional deps.
ensure_mod("langchain_core")
core_tools = ensure_mod("langchain_core.tools")
core_tools.Tool = type("Tool", (), {})
ensure_mod("langgraph")
lg_graph = ensure_mod("langgraph.graph")
lg_graph.StateGraph = type("StateGraph", (), {})
pyd = ensure_mod("pydantic")
pyd.BaseModel = type("BaseModel", (), {})
sys.path.insert(0, ".")
from dataflow_agent.graphbuilder.graph_builder import GenericGraphBuilder
from dataflow_agent.toolkits import tool_manager as tm_mod
# Reset the process-wide singleton for a clean repro.
tm_mod._tool_manager_instance = None
manager = tm_mod.get_tool_manager()
builder_a = GenericGraphBuilder(state_model=object, entry_point="start")
builder_b = GenericGraphBuilder(state_model=object, entry_point="start")
@builder_a.pre_tool("paper_content", "outline_agent")
def a_paper_content(state):
return state["paper_content"]
@builder_b.pre_tool("paper_content", "outline_agent")
def b_paper_content(state):
return state["paper_content"]
state_a = {"paper_content": "DOC_A"}
state_b = {"paper_content": "DOC_B"}
async def workflow_a():
builder_a._register_tools_for_role("outline_agent", state_a)
await asyncio.sleep(0.05)
return await manager.execute_pre_tools("outline_agent")
async def workflow_b():
await asyncio.sleep(0.01)
builder_b._register_tools_for_role("outline_agent", state_b)
return await manager.execute_pre_tools("outline_agent")
async def main():
a_result, b_result = await asyncio.gather(workflow_a(), workflow_b())
print("workflow_a_result=", a_result)
print("workflow_b_result=", b_result)
print("stored_callable_count=", len(manager.role_pre_tools["outline_agent"]))
print("stored_tool_names=", list(manager.role_pre_tools["outline_agent"].keys()))
asyncio.run(main())
Observed output:
workflow_a_result= {'paper_content': 'DOC_B'}
workflow_b_result= {'paper_content': 'DOC_B'}
stored_callable_count= 1
stored_tool_names= ['paper_content']
Expected behavior
Each workflow should execute its pre-tools against its own state.
In the example above:
- workflow A should read
DOC_A
- workflow B should read
DOC_B
The global tool registry should not let one workflow overwrite another workflow's state-bound pre-tool.
Screenshots
Not applicable.
Environment (please complete the following information):
- OS: Linux
- Browser (if applicable): N/A
- Version/Commit ID: reproduced against the current repository checkout on 2026-06-26
Related issues
I did not find a matching existing issue for this cross-workflow state leak.
Additional context
This looks especially risky because many shipped workflows register role-scoped pre-tools like:
paper_content
minueru_output
text_content
outline_feedback
and they all flow through the same singleton ToolManager.
So if two requests overlap in the same process and hit the same role/tool pair, one request can run with another request's state. In practice that can produce wrong outputs, but more importantly it can leak one user's document content or intermediate workflow state into another user's workflow.
Title: [BUG] Global ToolManager can leak state across concurrent workflows
Describe the bug
Paper2Any's workflow layer uses a process-wide singletonToolManager, butGenericGraphBuilder._register_tools_for_role(...)registers pre-tools as lambdas that capture the current workflow state object.Because those registrations are keyed only by
(role, tool_name), one workflow can overwrite another workflow's state-bound pre-tool when they use the same role/tool pair.That means overlapping workflows can execute with the wrong request state. In the reproduction below, workflow A registers
paper_content = DOC_A, workflow B registerspaper_content = DOC_B, and workflow A later executes againstDOC_B.To Reproduce
probe_tool_manager_leak.py.python probe_tool_manager_leak.py.paper_content.Observed output:
Expected behavior
Each workflow should execute its pre-tools against its own state.
In the example above:
DOC_ADOC_BThe global tool registry should not let one workflow overwrite another workflow's state-bound pre-tool.
Screenshots
Not applicable.
Environment (please complete the following information):
Related issues
I did not find a matching existing issue for this cross-workflow state leak.
Additional context
This looks especially risky because many shipped workflows register role-scoped pre-tools like:
paper_contentminueru_outputtext_contentoutline_feedbackand they all flow through the same singleton
ToolManager.So if two requests overlap in the same process and hit the same role/tool pair, one request can run with another request's state. In practice that can produce wrong outputs, but more importantly it can leak one user's document content or intermediate workflow state into another user's workflow.