Initial commit
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import shutil
|
||||
import unittest
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from opc.core.config import OPCConfig, PermissionsV2Config
|
||||
from opc.core.models import ApprovalAction, ApprovalDecision, PermissionResolution, RiskLevel, Task, TaskResult, TaskStatus
|
||||
from opc.layer3_agent.runtime_v2.permissions import ToolPermissionResolver
|
||||
from opc.layer3_agent.runtime_v2.runtime import NativeRuntimeV2
|
||||
from opc.layer3_agent.runtime_v2.streaming_tool_executor import StreamingToolExecutor
|
||||
from opc.layer3_agent.runtime_v2.subagents import SubagentManager
|
||||
from opc.layer3_agent.runtime_v2.tool_planner import ToolPlanner
|
||||
from opc.layer4_tools.registry import ToolDefinition, ToolRegistry
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _workspace_tempdir() -> Path:
|
||||
base = Path.cwd() / ".tmp-test" / f"runtime-hooks-{uuid.uuid4().hex}"
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
yield base
|
||||
finally:
|
||||
shutil.rmtree(base, ignore_errors=True)
|
||||
|
||||
|
||||
class _StubLLM:
|
||||
def __init__(self) -> None:
|
||||
self.config = type("Cfg", (), {"max_tokens": 2048})()
|
||||
|
||||
|
||||
class RuntimeHookBusTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_pre_hook_permission_gate_blocks_execution(self) -> None:
|
||||
registry = ToolRegistry()
|
||||
executed: list[str] = []
|
||||
|
||||
async def shell_tool(command: str) -> dict[str, str]:
|
||||
executed.append(command)
|
||||
return {"stdout": command}
|
||||
|
||||
registry.register(ToolDefinition(
|
||||
name="shell_exec",
|
||||
description="shell",
|
||||
parameters={"type": "object", "properties": {"command": {"type": "string"}}},
|
||||
func=shell_tool,
|
||||
concurrency_safe=False,
|
||||
read_only=False,
|
||||
requires_confirmation=True,
|
||||
))
|
||||
|
||||
async def approval_callback(tool, arguments, task, on_progress):
|
||||
_ = (tool, arguments, task, on_progress)
|
||||
return False, ApprovalDecision(
|
||||
action=ApprovalAction.ESCALATE,
|
||||
risk_level=RiskLevel.HIGH,
|
||||
rationale="Need approval first.",
|
||||
confidence=1.0,
|
||||
policy_source="test",
|
||||
)
|
||||
|
||||
runtime = NativeRuntimeV2(
|
||||
llm=_StubLLM(),
|
||||
tool_registry=registry,
|
||||
config=OPCConfig(),
|
||||
approval_callback=approval_callback,
|
||||
)
|
||||
task = Task(id="task-hook", session_id="sess-hook", project_id="proj1")
|
||||
hook_bus = runtime._build_tool_hook_bus(
|
||||
runtime_session_id="rt_hook",
|
||||
task=task,
|
||||
permission_resolver=ToolPermissionResolver(PermissionsV2Config()),
|
||||
)
|
||||
executor = StreamingToolExecutor(
|
||||
registry=registry,
|
||||
planner=ToolPlanner(registry),
|
||||
permission_resolver=ToolPermissionResolver(PermissionsV2Config()),
|
||||
hook_bus=hook_bus,
|
||||
)
|
||||
|
||||
results = await executor.execute([
|
||||
{"id": "call-1", "function": "shell_exec", "arguments": {"command": "git commit -m test"}},
|
||||
], task=task)
|
||||
|
||||
self.assertEqual(executed, [])
|
||||
self.assertFalse(results[0]["result"]["success"])
|
||||
self.assertEqual(results[0]["permission_decision"].resolution, PermissionResolution.ASK)
|
||||
self.assertEqual(results[0]["result"]["approval"]["policy_source"], "test")
|
||||
|
||||
async def test_parallel_batch_failure_converges_remaining_calls(self) -> None:
|
||||
registry = ToolRegistry()
|
||||
executed: list[str] = []
|
||||
|
||||
async def fail_read(path: str) -> dict[str, str]:
|
||||
raise RuntimeError(f"boom:{path}")
|
||||
|
||||
async def second_read(path: str) -> dict[str, str]:
|
||||
executed.append(path)
|
||||
return {"content": path}
|
||||
|
||||
registry.register(ToolDefinition(
|
||||
name="file_read",
|
||||
description="read",
|
||||
parameters={"type": "object", "properties": {"path": {"type": "string"}}},
|
||||
func=fail_read,
|
||||
concurrency_safe=True,
|
||||
read_only=True,
|
||||
))
|
||||
registry.register(ToolDefinition(
|
||||
name="web_fetch",
|
||||
description="fetch",
|
||||
parameters={"type": "object", "properties": {"path": {"type": "string"}}},
|
||||
func=second_read,
|
||||
concurrency_safe=True,
|
||||
read_only=True,
|
||||
))
|
||||
|
||||
runtime = NativeRuntimeV2(
|
||||
llm=_StubLLM(),
|
||||
tool_registry=registry,
|
||||
config=OPCConfig(),
|
||||
)
|
||||
hook_bus = runtime._build_tool_hook_bus(
|
||||
runtime_session_id="rt_parallel",
|
||||
task=Task(id="task-parallel", session_id="sess-parallel", project_id="proj1"),
|
||||
permission_resolver=ToolPermissionResolver(PermissionsV2Config()),
|
||||
)
|
||||
executor = StreamingToolExecutor(
|
||||
registry=registry,
|
||||
planner=ToolPlanner(registry, max_parallel_read_tools=1),
|
||||
permission_resolver=ToolPermissionResolver(PermissionsV2Config()),
|
||||
hook_bus=hook_bus,
|
||||
max_parallel_read_tools=1,
|
||||
converge_on_parallel_failure=True,
|
||||
)
|
||||
|
||||
results = await executor.execute([
|
||||
{"id": "call-a", "function": "file_read", "arguments": {"path": "a.txt"}},
|
||||
{"id": "call-b", "function": "web_fetch", "arguments": {"path": "b.txt"}},
|
||||
])
|
||||
|
||||
self.assertFalse(results[0]["result"]["success"])
|
||||
self.assertFalse(results[1]["result"]["success"])
|
||||
self.assertTrue(results[1]["result"]["converged"])
|
||||
self.assertEqual(executed, [])
|
||||
|
||||
|
||||
class SubagentPermissionBridgeTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_permission_bridge_routes_child_approval_through_parent_task(self) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class _ApprovalEngine:
|
||||
async def authorize_tool_call(self, **kwargs):
|
||||
captured["approval_kwargs"] = dict(kwargs)
|
||||
return True, ApprovalDecision(
|
||||
action=ApprovalAction.AUTO_APPROVE,
|
||||
risk_level=RiskLevel.LOW,
|
||||
rationale="approved",
|
||||
confidence=1.0,
|
||||
policy_source="human_escalation",
|
||||
metadata={"human_reply": "approve_session"},
|
||||
)
|
||||
|
||||
approval_engine = _ApprovalEngine()
|
||||
|
||||
class _ChildAgent:
|
||||
async def execute(self, child_task: Task) -> TaskResult:
|
||||
captured["child_task_metadata"] = dict(child_task.metadata)
|
||||
bridge = getattr(child_task, "_runtime_permission_bridge")
|
||||
tool = ToolDefinition(
|
||||
name="shell_exec",
|
||||
description="shell",
|
||||
parameters={"type": "object", "properties": {"command": {"type": "string"}}},
|
||||
func=lambda **_: None, # type: ignore[arg-type]
|
||||
concurrency_safe=False,
|
||||
read_only=False,
|
||||
requires_confirmation=True,
|
||||
)
|
||||
allowed, decision = await bridge(
|
||||
tool=tool,
|
||||
arguments={"command": "git status"},
|
||||
approval_engine=approval_engine,
|
||||
on_progress=None,
|
||||
)
|
||||
captured["allowed"] = allowed
|
||||
captured["decision"] = decision
|
||||
return TaskResult(status=TaskStatus.DONE, content="bridge-ok")
|
||||
|
||||
manager = SubagentManager(
|
||||
parent_task=Task(id="parent-task", session_id="parent-session", project_id="proj1"),
|
||||
config=OPCConfig(),
|
||||
child_agent_factory=lambda profile, allowed_tools, prompt, overrides: _ChildAgent(),
|
||||
runtime_session_id="rt_parent",
|
||||
)
|
||||
|
||||
result = await manager.spawn(
|
||||
profile="implement",
|
||||
prompt="Need approval",
|
||||
background=False,
|
||||
name="bridge-worker",
|
||||
)
|
||||
|
||||
self.assertTrue(result["success"])
|
||||
self.assertTrue(captured["allowed"])
|
||||
approval_kwargs = dict(captured["approval_kwargs"])
|
||||
self.assertEqual(approval_kwargs["task"].id, "parent-task")
|
||||
self.assertEqual(approval_kwargs["metadata"]["subagent_name"], "bridge-worker")
|
||||
self.assertEqual(captured["child_task_metadata"]["_permission_bridge_runtime_session_id"], "rt_parent")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user