patchpal 0.24.6__tar.gz → 0.25.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {patchpal-0.24.6/patchpal.egg-info → patchpal-0.25.0}/PKG-INFO +1 -1
  2. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/__init__.py +1 -1
  3. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/agent/function_calling.py +28 -5
  4. {patchpal-0.24.6 → patchpal-0.25.0/patchpal.egg-info}/PKG-INFO +1 -1
  5. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_enabled_tools.py +86 -0
  6. {patchpal-0.24.6 → patchpal-0.25.0}/LICENSE +0 -0
  7. {patchpal-0.24.6 → patchpal-0.25.0}/MANIFEST.in +0 -0
  8. {patchpal-0.24.6 → patchpal-0.25.0}/README.md +0 -0
  9. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/agent/__init__.py +0 -0
  10. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/agent/bedrock_profile_utils.py +0 -0
  11. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/agent/react.py +0 -0
  12. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/__init__.py +0 -0
  13. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/autopilot.py +0 -0
  14. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/interactive.py +0 -0
  15. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/mcp.py +0 -0
  16. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/sandbox.py +0 -0
  17. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/streaming.py +0 -0
  18. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/config.py +0 -0
  19. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/context.py +0 -0
  20. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/permissions.py +0 -0
  21. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/prompts/react_prompt.md +0 -0
  22. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/prompts/system_prompt.md +0 -0
  23. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/skills.py +0 -0
  24. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/__init__.py +0 -0
  25. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/audit.py +0 -0
  26. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/browser_tools.py +0 -0
  27. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/code_analysis.py +0 -0
  28. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/common.py +0 -0
  29. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/definitions.py +0 -0
  30. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/file_reading.py +0 -0
  31. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/file_writing.py +0 -0
  32. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/find_tool.py +0 -0
  33. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/grep_tool.py +0 -0
  34. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/image_handler.py +0 -0
  35. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/mcp.py +0 -0
  36. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/repo_map.py +0 -0
  37. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/shell_tools.py +0 -0
  38. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/todo_tools.py +0 -0
  39. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/tool_schema.py +0 -0
  40. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/user_interaction.py +0 -0
  41. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/web_tools.py +0 -0
  42. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal.egg-info/SOURCES.txt +0 -0
  43. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal.egg-info/dependency_links.txt +0 -0
  44. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal.egg-info/entry_points.txt +0 -0
  45. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal.egg-info/requires.txt +0 -0
  46. {patchpal-0.24.6 → patchpal-0.25.0}/patchpal.egg-info/top_level.txt +0 -0
  47. {patchpal-0.24.6 → patchpal-0.25.0}/pyproject.toml +0 -0
  48. {patchpal-0.24.6 → patchpal-0.25.0}/setup.cfg +0 -0
  49. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_agent.py +0 -0
  50. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_cli.py +0 -0
  51. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_config_dynamic.py +0 -0
  52. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_context.py +0 -0
  53. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_custom_tools.py +0 -0
  54. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_find_tool.py +0 -0
  55. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_guardrails.py +0 -0
  56. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_image_blocking.py +0 -0
  57. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_maximum_security.py +0 -0
  58. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_mcp_config.py +0 -0
  59. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_memory.py +0 -0
  60. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_memory_locations.py +0 -0
  61. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_operational_safety.py +0 -0
  62. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_optional_tools.py +0 -0
  63. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_permissions.py +0 -0
  64. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_react.py +0 -0
  65. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_reasoning_content.py +0 -0
  66. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_repo_map.py +0 -0
  67. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_simplified_prompt.py +0 -0
  68. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_skills.py +0 -0
  69. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_streaming.py +0 -0
  70. {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.24.6
3
+ Version: 0.25.0
4
4
  Summary: An agentic coding and automation assistant, supporting both local and cloud LLMs
5
5
  Author: PatchPal Contributors
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  """PatchPal - An open-source Claude Code clone implemented purely in Python."""
2
2
 
3
- __version__ = "0.24.6"
3
+ __version__ = "0.25.0"
4
4
 
5
5
  from patchpal.agent import create_agent, create_react_agent
6
6
  from patchpal.cli.autopilot import autopilot_loop
@@ -1358,13 +1358,36 @@ Repository root takes priority if both exist."""
1358
1358
  print(f"\033[1;31m✗ {tool_name}: Invalid arguments\033[0m\n", flush=True)
1359
1359
  else:
1360
1360
  # Get the tool function (check custom tools first, then built-in)
1361
- tool_func = self.custom_tool_funcs.get(tool_name) or TOOL_FUNCTIONS.get(
1362
- tool_name
1363
- )
1364
- if tool_func is None:
1361
+ tool_func = self.custom_tool_funcs.get(tool_name)
1362
+ tool_disabled = False # Track if tool was disabled
1363
+
1364
+ # If not a custom tool, check built-in tools with enabled_tools filter
1365
+ if tool_func is None and tool_name in TOOL_FUNCTIONS:
1366
+ # Only execute built-in tool if it's enabled
1367
+ # (enabled_tools=None means all enabled, otherwise check the list)
1368
+ if self.enabled_tools is None or tool_name in self.enabled_tools:
1369
+ tool_func = TOOL_FUNCTIONS.get(tool_name)
1370
+ else:
1371
+ # Tool is not enabled - reject with clear error
1372
+ tool_disabled = True
1373
+ available = (
1374
+ ", ".join(self.enabled_tools)
1375
+ if self.enabled_tools
1376
+ else "none (custom tools only)"
1377
+ )
1378
+ tool_result = f"Error: Tool '{tool_name}' is not enabled. Available built-in tools: {available}"
1379
+ print(
1380
+ f"\033[1;31m✗ {tool_name}: Tool not enabled\033[0m\n",
1381
+ flush=True,
1382
+ )
1383
+
1384
+ # Only mark as unknown if tool wasn't found AND wasn't disabled
1385
+ if tool_func is None and not tool_disabled:
1365
1386
  tool_result = f"Error: Unknown tool {tool_name}"
1366
1387
  print(f"\033[1;31m✗ Unknown tool: {tool_name}\033[0m\n", flush=True)
1367
- else:
1388
+
1389
+ # Only execute if we have a function and tool wasn't disabled
1390
+ if tool_func is not None and not tool_disabled:
1368
1391
  # Show tool call message
1369
1392
  if tool_name in self.custom_tool_funcs:
1370
1393
  # Custom tool - show generic message with args
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.24.6
3
+ Version: 0.25.0
4
4
  Summary: An agentic coding and automation assistant, supporting both local and cloud LLMs
5
5
  Author: PatchPal Contributors
6
6
  License-Expression: Apache-2.0
@@ -162,5 +162,91 @@ def test_env_var_with_spaces():
162
162
  del os.environ["PATCHPAL_ENABLED_TOOLS"]
163
163
 
164
164
 
165
+ def test_disabled_tool_execution_is_blocked(monkeypatch):
166
+ """Test that disabled tools cannot be executed even if LLM tries to call them.
167
+
168
+ This is a regression test for the bug where enabled_tools only filtered
169
+ the tool schemas sent to the LLM, but didn't prevent execution if the LLM
170
+ hallucinated a call to a disabled tool.
171
+ """
172
+ # Disable streaming for simpler mocking. monkeypatch auto-restores the
173
+ # env var on teardown, so this won't pollute other tests.
174
+ monkeypatch.setenv("PATCHPAL_STREAM_OUTPUT", "false")
175
+
176
+ with patch("patchpal.agent.function_calling.litellm.completion") as mock_completion:
177
+ # Create a mock tool call for run_shell (which is NOT enabled)
178
+ mock_tool_call = MagicMock()
179
+ mock_tool_call.id = "call_123"
180
+ mock_tool_call.function.name = "run_shell"
181
+ mock_tool_call.function.arguments = '{"cmd": "echo test"}'
182
+
183
+ # First call: LLM tries to call run_shell (not enabled)
184
+ mock_response1 = MagicMock()
185
+ mock_response1.choices = [MagicMock()]
186
+ mock_response1.choices[0].message.content = "Running command"
187
+ mock_response1.choices[0].message.tool_calls = [mock_tool_call]
188
+ mock_response1.usage = MagicMock(
189
+ prompt_tokens=10,
190
+ completion_tokens=5,
191
+ cache_creation_input_tokens=0,
192
+ cache_read_input_tokens=0,
193
+ )
194
+
195
+ # Second call: LLM responds after seeing the error
196
+ mock_response2 = MagicMock()
197
+ mock_response2.choices = [MagicMock()]
198
+ mock_response2.choices[0].message.content = "Tool not available"
199
+ mock_response2.choices[0].message.tool_calls = None
200
+ mock_response2.usage = MagicMock(
201
+ prompt_tokens=10,
202
+ completion_tokens=5,
203
+ cache_creation_input_tokens=0,
204
+ cache_read_input_tokens=0,
205
+ )
206
+
207
+ mock_completion.side_effect = [mock_response1, mock_response2]
208
+
209
+ # Create agent with only read_file enabled (run_shell is NOT enabled)
210
+ agent = create_agent(enabled_tools=["read_file"])
211
+
212
+ # Run - should reject run_shell execution
213
+ agent.run("Test message")
214
+
215
+ # Verify run_shell was blocked (check the agent's message history)
216
+ tool_results = [msg for msg in agent.messages if msg.get("role") == "tool"]
217
+ assert len(tool_results) == 1, f"Expected 1 tool result, got {len(tool_results)}"
218
+
219
+ # The tool result should contain an error about the tool not being enabled
220
+ error_message = tool_results[0]["content"]
221
+ assert "not enabled" in error_message
222
+ assert "run_shell" in error_message
223
+
224
+
225
+ def test_enabled_tool_execution_is_allowed():
226
+ """Test that enabled tools CAN be executed when LLM calls them."""
227
+ # This is a simple verification that enabled tools work - detailed behavior
228
+ # is tested in integration tests
229
+ agent = create_agent(enabled_tools=["read_file", "write_file"])
230
+
231
+ # Just verify the agent was created with correct enabled_tools
232
+ assert agent.enabled_tools == ["read_file", "write_file"]
233
+ assert len(agent.custom_tools) == 0
234
+
235
+
236
+ def test_empty_enabled_tools_blocks_all_builtins():
237
+ """Test that enabled_tools=[] blocks all built-in tools (only custom tools allowed)."""
238
+
239
+ def custom_tool(x: int) -> str:
240
+ """A custom tool."""
241
+ return f"Result: {x}"
242
+
243
+ agent = create_agent(enabled_tools=[], custom_tools=[custom_tool])
244
+
245
+ # Verify configuration
246
+ assert agent.enabled_tools == []
247
+ assert len(agent.custom_tools) == 1
248
+ assert agent.custom_tools[0].__name__ == "custom_tool"
249
+
250
+
165
251
  if __name__ == "__main__":
166
252
  pytest.main([__file__, "-v"])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes