fix(exec): isolate exec session managers

This commit is contained in:
yu-xin-c
2026-07-18 17:35:56 +08:00
committed by Xubin Ren
parent 7ac9a46978
commit 995cc44e89
8 changed files with 132 additions and 12 deletions
@@ -0,0 +1,40 @@
from types import SimpleNamespace
from unittest.mock import MagicMock
from nanobot.agent.loop import AgentLoop
from nanobot.bus.queue import MessageBus
def _provider() -> MagicMock:
provider = MagicMock()
provider.get_default_model.return_value = "test-model"
provider.generation = SimpleNamespace(max_tokens=4096, temperature=0.1, reasoning_effort=None)
return provider
def test_agent_loops_do_not_share_exec_session_managers(tmp_path):
loop_a = AgentLoop(
bus=MessageBus(),
provider=_provider(),
workspace=tmp_path / "a",
model="test-model",
context_window_tokens=4096,
)
loop_b = AgentLoop(
bus=MessageBus(),
provider=_provider(),
workspace=tmp_path / "b",
model="test-model",
context_window_tokens=4096,
)
exec_a = loop_a.tools.get("exec")
stdin_a = loop_a.tools.get("write_stdin")
list_a = loop_a.tools.get("list_exec_sessions")
exec_b = loop_b.tools.get("exec")
assert exec_a._session_manager is loop_a._exec_session_manager
assert stdin_a._manager is loop_a._exec_session_manager
assert list_a._manager is loop_a._exec_session_manager
assert exec_b._session_manager is loop_b._exec_session_manager
assert loop_a._exec_session_manager is not loop_b._exec_session_manager
+51
View File
@@ -7,6 +7,7 @@ import subprocess
import sys
import time
from nanobot.agent.tools.context import RequestContext, bind_request_context, reset_request_context
from nanobot.agent.tools.exec_session import (
ExecSessionManager,
ListExecSessionsTool,
@@ -398,6 +399,56 @@ def test_list_exec_sessions_reports_running_commands(tmp_path):
assert "Session terminated." in cleanup
def test_exec_sessions_are_scoped_to_request_session_key(tmp_path):
async def run() -> tuple[str, str, str, str, str, str]:
manager = ExecSessionManager()
exec_tool = ExecTool(working_dir=str(tmp_path), timeout=5, session_manager=manager)
list_tool = ListExecSessionsTool(manager=manager)
stdin_tool = WriteStdinTool(manager=manager)
command = _python_command(
"import time; print('ready', flush=True); time.sleep(5)"
)
token_a = bind_request_context(
RequestContext(channel="cli", chat_id="a", session_key="cli:a")
)
try:
initial = await exec_tool.execute(command=command, yield_time_ms=100)
sid = _session_id(initial)
owner_listing = await list_tool.execute()
finally:
reset_request_context(token_a)
unbound_listing = await list_tool.execute()
token_b = bind_request_context(
RequestContext(channel="cli", chat_id="b", session_key="cli:b")
)
try:
other_listing = await list_tool.execute()
other_write = await stdin_tool.execute(session_id=sid, yield_time_ms=0)
finally:
reset_request_context(token_b)
token_a = bind_request_context(
RequestContext(channel="cli", chat_id="a", session_key="cli:a")
)
try:
cleanup = await stdin_tool.execute(session_id=sid, terminate=True, yield_time_ms=0)
finally:
reset_request_context(token_a)
return sid, owner_listing, unbound_listing, other_listing, other_write, cleanup
sid, owner_listing, unbound_listing, other_listing, other_write, cleanup = asyncio.run(run())
assert sid in owner_listing
assert unbound_listing == "No active exec sessions."
assert other_listing == "No active exec sessions."
assert other_write == f"Error: exec session not found: {sid!r}"
assert "Session terminated." in cleanup
def test_list_exec_sessions_reports_empty_state():
result = asyncio.run(ListExecSessionsTool(manager=ExecSessionManager()).execute())
+29 -2
View File
@@ -57,8 +57,8 @@ def test_tool_context_has_required_fields():
field_names = {f.name for f in fields(ToolContext)}
required = {
"config", "workspace", "bus", "subagent_manager",
"cron_service", "file_state_store", "provider_snapshot_loader",
"image_generation_provider_configs", "timezone",
"cron_service", "exec_session_manager", "file_state_store",
"provider_snapshot_loader", "image_generation_provider_configs", "timezone",
}
assert required <= field_names
@@ -68,6 +68,7 @@ def test_tool_context_defaults():
assert ctx.bus is None
assert ctx.subagent_manager is None
assert ctx.cron_service is None
assert ctx.exec_session_manager is None
assert ctx.provider_snapshot_loader is None
assert ctx.image_generation_provider_configs is None
assert ctx.timezone == "UTC"
@@ -137,6 +138,32 @@ def test_loader_registers_exec_with_real_tools_config(tmp_path):
assert registry.has("exec")
def test_loader_wires_shared_exec_session_manager(tmp_path):
from types import SimpleNamespace
from nanobot.agent.tools.exec_session import ExecSessionManager
from nanobot.agent.tools.registry import ToolRegistry
from nanobot.config.schema import ToolsConfig
manager = ExecSessionManager()
ctx = ToolContext(
config=ToolsConfig(),
workspace=str(tmp_path),
subagent_manager=SimpleNamespace(
get_running_count=lambda: 0,
max_concurrent_subagents=4,
),
exec_session_manager=manager,
timezone="UTC",
)
registry = ToolRegistry()
ToolLoader().load(ctx, registry)
assert registry.get("exec")._session_manager is manager
assert registry.get("write_stdin")._manager is manager
assert registry.get("list_exec_sessions")._manager is manager
# --- Task 4: _FsTool.create() ---