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.
- {patchpal-0.24.6/patchpal.egg-info → patchpal-0.25.0}/PKG-INFO +1 -1
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/__init__.py +1 -1
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/agent/function_calling.py +28 -5
- {patchpal-0.24.6 → patchpal-0.25.0/patchpal.egg-info}/PKG-INFO +1 -1
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_enabled_tools.py +86 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/LICENSE +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/MANIFEST.in +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/README.md +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/agent/__init__.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/agent/bedrock_profile_utils.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/agent/react.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/__init__.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/autopilot.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/interactive.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/mcp.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/sandbox.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/cli/streaming.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/config.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/context.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/permissions.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/prompts/react_prompt.md +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/prompts/system_prompt.md +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/skills.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/__init__.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/audit.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/browser_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/code_analysis.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/common.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/definitions.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/file_reading.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/file_writing.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/find_tool.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/grep_tool.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/image_handler.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/mcp.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/repo_map.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/shell_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/todo_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/tool_schema.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/user_interaction.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal/tools/web_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal.egg-info/SOURCES.txt +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal.egg-info/dependency_links.txt +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal.egg-info/entry_points.txt +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal.egg-info/requires.txt +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/patchpal.egg-info/top_level.txt +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/pyproject.toml +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/setup.cfg +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_agent.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_cli.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_config_dynamic.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_context.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_custom_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_find_tool.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_guardrails.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_image_blocking.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_maximum_security.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_mcp_config.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_memory.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_memory_locations.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_operational_safety.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_optional_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_permissions.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_react.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_reasoning_content.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_repo_map.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_simplified_prompt.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_skills.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_streaming.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.0}/tests/test_tools.py +0 -0
|
@@ -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)
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|