test_runner_policy.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import pytest
  2. from agent.core.presets import AgentPreset, register_preset
  3. from agent.core.runner import AgentRunner, RunConfig
  4. from agent.orchestration.models import AgentRole, CompletionPolicy
  5. from agent.tools.models import ToolCapability, ToolResult
  6. from agent.tools.registry import ToolRegistry
  7. from agent.trace.models import Trace
  8. def _schema(name):
  9. return {
  10. "type": "function",
  11. "function": {"name": name, "description": name, "parameters": {"type": "object", "properties": {}}},
  12. }
  13. @pytest.mark.asyncio
  14. async def test_terminal_tool_completes_without_final_assistant_text(tmp_path):
  15. registry = ToolRegistry()
  16. async def finish():
  17. return ToolResult(
  18. title="done", output="submitted", terminate_run=True, result_summary="structured-result"
  19. )
  20. registry.register(finish, schema=_schema("finish"), groups=["test"])
  21. calls = 0
  22. async def llm_call(**kwargs):
  23. nonlocal calls
  24. calls += 1
  25. return {
  26. "content": "",
  27. "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "finish", "arguments": "{}"}}],
  28. "finish_reason": "tool_calls",
  29. }
  30. from agent.trace.store import FileSystemTraceStore
  31. runner = AgentRunner(
  32. trace_store=FileSystemTraceStore(str(tmp_path)),
  33. tool_registry=registry,
  34. llm_call=llm_call,
  35. )
  36. result = await runner.run_result(
  37. [{"role": "user", "content": "finish"}],
  38. RunConfig(tools=["finish"], tool_groups=[], knowledge=_disabled_knowledge()),
  39. )
  40. assert calls == 1
  41. assert result["status"] == "completed"
  42. assert result["summary"] == "structured-result"
  43. @pytest.mark.asyncio
  44. async def test_execution_layer_rejects_forged_worker_tool_call():
  45. registry = ToolRegistry()
  46. executed = False
  47. async def agent():
  48. nonlocal executed
  49. executed = True
  50. return "bad"
  51. registry.register(agent, schema=_schema("agent"))
  52. register_preset(
  53. "test_worker_policy",
  54. AgentPreset(role=AgentRole.WORKER, allowed_tools=["agent"], max_iterations=5, skills=[]),
  55. )
  56. runner = AgentRunner(tool_registry=registry, llm_call=lambda **_: None, task_coordinator=object())
  57. config = RunConfig(
  58. agent_type="test_worker_policy",
  59. completion_policy=CompletionPolicy.EXPLICIT_VALIDATION,
  60. tools=["agent"],
  61. tool_groups=None,
  62. )
  63. trace = Trace(trace_id="worker-trace", mode="agent", agent_role="worker", context={})
  64. result = await runner._execute_authorized_tool(
  65. "agent", {}, "call", config, trace, None, 1
  66. )
  67. assert result["error"] == "unauthorized_tool"
  68. assert executed is False
  69. @pytest.mark.asyncio
  70. async def test_framework_context_overrides_host_spoofing():
  71. registry = ToolRegistry()
  72. register_preset(
  73. "test_worker_context",
  74. AgentPreset(role=AgentRole.WORKER, allowed_tools=[], max_iterations=5, skills=[]),
  75. )
  76. runner = AgentRunner(tool_registry=registry, llm_call=lambda **_: None, task_coordinator="real")
  77. config = RunConfig(
  78. agent_type="test_worker_context",
  79. completion_policy=CompletionPolicy.EXPLICIT_VALIDATION,
  80. context={"role": "planner", "task_id": "spoofed", "coordinator": "fake"},
  81. )
  82. trace = Trace(
  83. trace_id="trace", mode="agent", agent_role="worker",
  84. context={"root_trace_id": "root", "task_id": "real-task", "attempt_id": "attempt"},
  85. )
  86. context = runner._build_protected_tool_context(config, trace, None, 1)
  87. assert context["role"] == "worker"
  88. assert context["task_id"] == "real-task"
  89. assert context["coordinator"] == "real"
  90. @pytest.mark.asyncio
  91. async def test_validator_capabilities_hide_and_reject_arbitrary_write_tool():
  92. registry = ToolRegistry()
  93. executed = False
  94. async def db_modify():
  95. nonlocal executed
  96. executed = True
  97. return "modified"
  98. async def db_lookup():
  99. return "row"
  100. registry.register(
  101. db_modify,
  102. schema=_schema("db_modify"),
  103. capabilities=[ToolCapability.WRITE],
  104. )
  105. registry.register(
  106. db_lookup,
  107. schema=_schema("db_lookup"),
  108. capabilities=[ToolCapability.READ],
  109. )
  110. register_preset(
  111. "test_validator_capabilities",
  112. AgentPreset(
  113. role=AgentRole.VALIDATOR,
  114. allowed_tools=["db_modify", "db_lookup"],
  115. max_iterations=5,
  116. skills=[],
  117. ),
  118. )
  119. runner = AgentRunner(tool_registry=registry, llm_call=lambda **_: None, task_coordinator=object())
  120. config = RunConfig(
  121. agent_type="test_validator_capabilities",
  122. completion_policy=CompletionPolicy.EXPLICIT_VALIDATION,
  123. tools=["db_modify", "db_lookup"],
  124. )
  125. names = {
  126. item["function"]["name"] for item in runner._get_run_tool_schemas(config)
  127. }
  128. assert names == {"db_lookup"}
  129. trace = Trace(trace_id="validator", mode="agent", agent_role="validator", context={})
  130. result = await runner._execute_authorized_tool(
  131. "db_modify", {}, "forged", config, trace, None, 1
  132. )
  133. assert result["error"] == "unauthorized_tool"
  134. assert executed is False
  135. @pytest.mark.asyncio
  136. async def test_worker_rejects_agent_spawn_capability_with_unrelated_name():
  137. registry = ToolRegistry()
  138. executed = False
  139. async def spawn_helper():
  140. nonlocal executed
  141. executed = True
  142. return "spawned"
  143. registry.register(
  144. spawn_helper,
  145. schema=_schema("spawn_helper"),
  146. capabilities=[ToolCapability.AGENT_SPAWN],
  147. )
  148. register_preset(
  149. "test_worker_spawn_capability",
  150. AgentPreset(
  151. role=AgentRole.WORKER,
  152. allowed_tools=["spawn_helper"],
  153. max_iterations=5,
  154. skills=[],
  155. ),
  156. )
  157. runner = AgentRunner(tool_registry=registry, llm_call=lambda **_: None, task_coordinator=object())
  158. config = RunConfig(
  159. agent_type="test_worker_spawn_capability",
  160. completion_policy=CompletionPolicy.EXPLICIT_VALIDATION,
  161. tools=["spawn_helper"],
  162. )
  163. assert runner._get_run_tool_schemas(config) == []
  164. trace = Trace(trace_id="worker", mode="agent", agent_role="worker", context={})
  165. result = await runner._execute_authorized_tool(
  166. "spawn_helper", {}, "forged", config, trace, None, 1
  167. )
  168. assert result["error"] == "unauthorized_tool"
  169. assert executed is False
  170. @pytest.mark.asyncio
  171. async def test_unclassified_tool_fails_closed_only_in_explicit_mode():
  172. registry = ToolRegistry()
  173. calls = 0
  174. async def mystery_tool():
  175. nonlocal calls
  176. calls += 1
  177. return "ok"
  178. registry.register(mystery_tool, schema=_schema("mystery_tool"))
  179. register_preset(
  180. "test_unclassified_worker",
  181. AgentPreset(
  182. role=AgentRole.WORKER,
  183. allowed_tools=["mystery_tool"],
  184. max_iterations=5,
  185. skills=[],
  186. ),
  187. )
  188. runner = AgentRunner(tool_registry=registry, llm_call=lambda **_: None, task_coordinator=object())
  189. explicit = RunConfig(
  190. agent_type="test_unclassified_worker",
  191. completion_policy=CompletionPolicy.EXPLICIT_VALIDATION,
  192. tools=["mystery_tool"],
  193. )
  194. worker_trace = Trace(trace_id="worker", mode="agent", agent_role="worker", context={})
  195. rejected = await runner._execute_authorized_tool(
  196. "mystery_tool", {}, "explicit", explicit, worker_trace, None, 1
  197. )
  198. assert rejected["error"] == "unauthorized_tool"
  199. assert calls == 0
  200. legacy = RunConfig(tools=["mystery_tool"], tool_groups=[])
  201. legacy_trace = Trace(trace_id="legacy", mode="agent", agent_role="legacy", context={})
  202. assert {x["function"]["name"] for x in runner._get_run_tool_schemas(legacy)} == {"mystery_tool"}
  203. assert await runner._execute_authorized_tool(
  204. "mystery_tool", {}, "legacy", legacy, legacy_trace, None, 1
  205. ) == "ok"
  206. assert calls == 1
  207. def _disabled_knowledge():
  208. from agent.tools.builtin.knowledge import KnowledgeConfig
  209. return KnowledgeConfig(enable_extraction=False, enable_completion_extraction=False, enable_injection=False)