This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/src/timmy/tool_safety.py
Alexander Whitestone 904a7c564e feat: migrate to Agno native HITL tool confirmation flow (#158)
Replace the homebrew regex-based tool extraction and manual dispatch
(tool_executor.py) with Agno's built-in Human-In-The-Loop confirmation:

- Toolkit(requires_confirmation_tools=...) marks dangerous tools
- agent.run() returns RunOutput with status=paused when confirmation needed
- RunRequirement.confirm()/reject() + agent.continue_run() resumes execution

Dashboard and Discord vendor both use the native flow. DuckDuckGo import
isolated so its absence doesn't kill all tools. Test stubs cleaned up
(agno is a real dependency, only truly optional packages stubbed).

1384 tests pass in parallel (~14s).

Co-authored-by: Trip T <trip@local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:54:04 -04:00

124 lines
3.7 KiB
Python

"""Tool safety classification and tool-call extraction helpers.
Classifies tools into tiers based on their potential impact:
- DANGEROUS: Can modify filesystem, execute code, or change system state.
Requires user confirmation before execution.
- SAFE: Read-only or purely computational. Executes without confirmation.
Also provides shared helpers for extracting hallucinated tool calls from
model output and formatting them for human review. Used by both the
Discord vendor and the dashboard chat route.
"""
import json
import re
# ---------------------------------------------------------------------------
# Tool classification
# ---------------------------------------------------------------------------
# Tools that require confirmation before execution.
DANGEROUS_TOOLS = frozenset(
{
"shell",
"python",
"write_file",
"aider",
"plan_and_execute",
}
)
# Tools that are safe to execute without confirmation.
SAFE_TOOLS = frozenset(
{
"web_search",
"calculator",
"memory_search",
"memory_read",
"memory_write",
"read_file",
"list_files",
"consult_grok",
"get_system_info",
"check_ollama_health",
"get_memory_status",
"list_swarm_agents",
}
)
def requires_confirmation(tool_name: str) -> bool:
"""Check if a tool requires user confirmation before execution.
Unknown tools default to requiring confirmation (safe-by-default).
"""
if tool_name in SAFE_TOOLS:
return False
return True
# ---------------------------------------------------------------------------
# Tool call extraction from model output
# ---------------------------------------------------------------------------
_TOOL_CALL_RE = re.compile(
r'\{\s*"name"\s*:\s*"([^"]+?)"\s*,\s*"(?:parameters|arguments)"\s*:\s*(\{.*?\})\s*\}',
re.DOTALL,
)
def extract_tool_calls(text: str) -> list[tuple[str, dict]]:
"""Extract hallucinated tool calls from model output.
Returns list of (tool_name, arguments_dict) tuples.
Handles both ``"arguments"`` and ``"parameters"`` JSON keys.
"""
if not text:
return []
results = []
for match in _TOOL_CALL_RE.finditer(text):
tool_name = match.group(1)
try:
args = json.loads(match.group(2))
except json.JSONDecodeError:
continue
results.append((tool_name, args))
return results
# ---------------------------------------------------------------------------
# Formatting helpers
# ---------------------------------------------------------------------------
def format_action_description(tool_name: str, tool_args: dict) -> str:
"""Format a human-readable description of a tool action."""
if tool_name == "shell":
cmd = tool_args.get("command") or tool_args.get("args", "")
if isinstance(cmd, list):
cmd = " ".join(cmd)
return f"Run shell command:\n`{cmd}`"
elif tool_name == "write_file":
path = tool_args.get("file_name", "unknown")
size = len(tool_args.get("contents", ""))
return f"Write file: `{path}` ({size} chars)"
elif tool_name == "python":
code = tool_args.get("code", "")[:200]
return f"Execute Python:\n```python\n{code}\n```"
else:
args_str = json.dumps(tool_args, indent=2)[:300]
return f"Execute `{tool_name}` with args:\n```json\n{args_str}\n```"
def get_impact_level(tool_name: str) -> str:
"""Return the impact level for a tool (high, medium, or low)."""
high_impact = {"shell", "python"}
medium_impact = {"write_file", "aider", "plan_and_execute"}
if tool_name in high_impact:
return "high"
if tool_name in medium_impact:
return "medium"
return "low"