core/agent: danger classification overrides YOLO tag in confirm pipeline

This commit is contained in:
2026-07-29 01:37:17 +08:00
parent 904ac3d982
commit 60f9c9b56d
3 changed files with 13 additions and 15 deletions
+4 -8
View File
@@ -392,14 +392,15 @@ class ToolRegistry:
*, *,
tags: ToolTag, tags: ToolTag,
) -> str | None: ) -> 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):: Pipeline (soft gate only; hard denylists stay in tool handlers)::
no soft reason → run no soft reason → run
YOLO mode and (tags & YOLO) → allow
(tags & TRUSTABLE) and grant exists → 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: if self._danger is None:
return None return None
@@ -408,11 +409,6 @@ class ToolRegistry:
return None return None
async with self._confirm_lock: 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): if self._grant_allows(name, tags=tags):
return None return None
return await self._prompt_soft_confirm(name, args, reason, tags=tags) return await self._prompt_soft_confirm(name, args, reason, tags=tags)
+2 -1
View File
@@ -446,7 +446,8 @@ async def test_yolo_rebuilds_tool_registry(tmp_path: Path) -> None:
) )
try: try:
assert st.agent.tools is not None 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.soft_confirm is True
assert st.agent.tools.yolo is False assert st.agent.tools.yolo is False
st.set_yolo("on") st.set_yolo("on")
+7 -6
View File
@@ -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 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] = [] calls: list[str] = []
@tool(tags=ToolTag.LOCAL | ToolTag.YOLO, register=False) @tool(tags=ToolTag.LOCAL | ToolTag.YOLO, register=False)
async def yolo_ok() -> str: async def yolo_tool() -> str:
return "y" return "y"
@tool(tags=ToolTag.LOCAL, register=False) @tool(tags=ToolTag.LOCAL, register=False)
@@ -27,15 +28,15 @@ async def test_yolo_only_skips_yolo_tagged_tools() -> None:
return True return True
reg = ToolRegistry( reg = ToolRegistry(
[yolo_ok, no_yolo], [yolo_tool, no_yolo],
danger=danger, danger=danger,
on_confirm=confirm, on_confirm=confirm,
yolo=True, yolo=True,
) )
assert await reg.execute("yolo_ok", "{}") == "y" assert await reg.execute("yolo_tool", "{}") == "y"
assert calls == [] assert calls == ["yolo_tool"]
assert await reg.execute("no_yolo", "{}") == "n" assert await reg.execute("no_yolo", "{}") == "n"
assert calls == ["no_yolo"] assert calls == ["yolo_tool", "no_yolo"]
async def test_trustable_grant_once() -> None: async def test_trustable_grant_once() -> None: