forked from Rockachopa/Timmy-time-dashboard
[loop-cycle-1] feat: tool allowlist for autonomous operation (#69)
Add config/allowlist.yaml — YAML-driven gate that auto-approves bounded tool calls when no human is present. When Timmy runs with --autonomous or stdin is not a terminal, tool calls are checked against allowlist: matched → auto-approved, else → rejected. Changes: - config/allowlist.yaml: shell prefixes, deny patterns, path rules - tool_safety.py: is_allowlisted() checks tools against YAML rules - cli.py: --autonomous flag, _is_interactive() detection - 44 new allowlist tests, 8 updated CLI tests Closes #69
This commit is contained in:
@@ -177,8 +177,11 @@ def test_handle_tool_confirmation_approve():
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.continue_run.return_value = completed_run
|
||||
|
||||
# Simulate user typing "y" at the prompt
|
||||
with patch("timmy.cli.typer.confirm", return_value=True):
|
||||
# Simulate user typing "y" at the prompt (mock interactive terminal)
|
||||
with (
|
||||
patch("timmy.cli._is_interactive", return_value=True),
|
||||
patch("timmy.cli.typer.confirm", return_value=True),
|
||||
):
|
||||
result = _handle_tool_confirmation(mock_agent, paused_run, "cli")
|
||||
|
||||
mock_req.confirm.assert_called_once()
|
||||
@@ -198,7 +201,10 @@ def test_handle_tool_confirmation_reject():
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.continue_run.return_value = completed_run
|
||||
|
||||
with patch("timmy.cli.typer.confirm", return_value=False):
|
||||
with (
|
||||
patch("timmy.cli._is_interactive", return_value=True),
|
||||
patch("timmy.cli.typer.confirm", return_value=False),
|
||||
):
|
||||
_handle_tool_confirmation(mock_agent, paused_run, "cli")
|
||||
|
||||
mock_req.reject.assert_called_once()
|
||||
@@ -225,8 +231,49 @@ def test_handle_tool_confirmation_continue_error():
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.continue_run.side_effect = Exception("connection lost")
|
||||
|
||||
with patch("timmy.cli.typer.confirm", return_value=True):
|
||||
with (
|
||||
patch("timmy.cli._is_interactive", return_value=True),
|
||||
patch("timmy.cli.typer.confirm", return_value=True),
|
||||
):
|
||||
result = _handle_tool_confirmation(mock_agent, paused_run, "cli")
|
||||
|
||||
# Should return the original paused run, not crash
|
||||
assert result is paused_run
|
||||
|
||||
|
||||
def test_handle_tool_confirmation_autonomous_allowlisted():
|
||||
"""In autonomous mode, allowlisted tools should be auto-approved."""
|
||||
paused_run, mock_req = _make_paused_run(
|
||||
tool_name="shell", tool_args={"command": "pytest tests/ -x"}
|
||||
)
|
||||
|
||||
completed_run = MagicMock()
|
||||
completed_run.status = "COMPLETED"
|
||||
completed_run.active_requirements = []
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.continue_run.return_value = completed_run
|
||||
|
||||
with patch("timmy.cli.is_allowlisted", return_value=True):
|
||||
_handle_tool_confirmation(mock_agent, paused_run, "cli", autonomous=True)
|
||||
|
||||
mock_req.confirm.assert_called_once()
|
||||
mock_req.reject.assert_not_called()
|
||||
|
||||
|
||||
def test_handle_tool_confirmation_autonomous_not_allowlisted():
|
||||
"""In autonomous mode, non-allowlisted tools should be auto-rejected."""
|
||||
paused_run, mock_req = _make_paused_run(tool_name="shell", tool_args={"command": "rm -rf /"})
|
||||
|
||||
completed_run = MagicMock()
|
||||
completed_run.status = "COMPLETED"
|
||||
completed_run.active_requirements = []
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.continue_run.return_value = completed_run
|
||||
|
||||
with patch("timmy.cli.is_allowlisted", return_value=False):
|
||||
_handle_tool_confirmation(mock_agent, paused_run, "cli", autonomous=True)
|
||||
|
||||
mock_req.reject.assert_called_once()
|
||||
mock_req.confirm.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user