diff --git a/src/plyngent/agent/tools.py b/src/plyngent/agent/tools.py index 1ac7254..db2afe6 100644 --- a/src/plyngent/agent/tools.py +++ b/src/plyngent/agent/tools.py @@ -392,14 +392,15 @@ class ToolRegistry: *, tags: ToolTag, ) -> str | None: - """Soft-confirm gate driven by danger reason + tags + grants + YOLO. + """Soft-confirm gate driven by danger reason + tags + grants. Pipeline (soft gate only; hard denylists stay in tool handlers):: no soft reason → run - YOLO mode and (tags & YOLO) → allow (tags & TRUSTABLE) and grant exists → allow - else on_confirm; on approve + TRUSTABLE → store grant + else → on_confirm (decorator YOLO tag does not override + dangerous invocations — internal danger classification + always takes precedence) """ if self._danger is None: return None @@ -408,11 +409,6 @@ class ToolRegistry: return None async with self._confirm_lock: - reason = self._danger(name, args) - if reason is None: - return None - if self._yolo and (tags & ToolTag.YOLO): - return None if self._grant_allows(name, tags=tags): return None return await self._prompt_soft_confirm(name, args, reason, tags=tags) diff --git a/tests/test_cli/test_repl_commands.py b/tests/test_cli/test_repl_commands.py index 228523b..a7aaa2c 100644 --- a/tests/test_cli/test_repl_commands.py +++ b/tests/test_cli/test_repl_commands.py @@ -446,7 +446,8 @@ async def test_yolo_rebuilds_tool_registry(tmp_path: Path) -> None: ) try: assert st.agent.tools is not None - # Soft-confirm hooks stay installed; YOLO only auto-allows YOLO-tagged tools. + # Soft-confirm hooks stay installed; YOLO mode does not bypass + # dangerous invocations — internal danger classification takes precedence. assert st.agent.tools.soft_confirm is True assert st.agent.tools.yolo is False st.set_yolo("on") diff --git a/tests/test_tools/test_confirm_tags.py b/tests/test_tools/test_confirm_tags.py index 290fd1e..6733231 100644 --- a/tests/test_tools/test_confirm_tags.py +++ b/tests/test_tools/test_confirm_tags.py @@ -8,11 +8,12 @@ from plyngent.tools.grants import clear_grants, has_grant, hydrate_grants, persi from plyngent.tools.view import MemoryViewStore, session_data_view -async def test_yolo_only_skips_yolo_tagged_tools() -> None: +async def test_danger_overrides_yolo_tag() -> None: + """Decorator YOLO tag does not bypass confirm when danger flags the call.""" calls: list[str] = [] @tool(tags=ToolTag.LOCAL | ToolTag.YOLO, register=False) - async def yolo_ok() -> str: + async def yolo_tool() -> str: return "y" @tool(tags=ToolTag.LOCAL, register=False) @@ -27,15 +28,15 @@ async def test_yolo_only_skips_yolo_tagged_tools() -> None: return True reg = ToolRegistry( - [yolo_ok, no_yolo], + [yolo_tool, no_yolo], danger=danger, on_confirm=confirm, yolo=True, ) - assert await reg.execute("yolo_ok", "{}") == "y" - assert calls == [] + assert await reg.execute("yolo_tool", "{}") == "y" + assert calls == ["yolo_tool"] assert await reg.execute("no_yolo", "{}") == "n" - assert calls == ["no_yolo"] + assert calls == ["yolo_tool", "no_yolo"] async def test_trustable_grant_once() -> None: