patchpal 0.24.6__tar.gz → 0.25.1__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.1}/PKG-INFO +1 -1
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/__init__.py +1 -1
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/agent/function_calling.py +28 -5
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/sandbox.py +85 -13
- {patchpal-0.24.6 → patchpal-0.25.1/patchpal.egg-info}/PKG-INFO +1 -1
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_enabled_tools.py +86 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/LICENSE +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/MANIFEST.in +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/README.md +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/agent/__init__.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/agent/bedrock_profile_utils.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/agent/react.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/__init__.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/autopilot.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/interactive.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/mcp.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/streaming.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/config.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/context.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/permissions.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/prompts/react_prompt.md +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/prompts/system_prompt.md +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/skills.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/__init__.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/audit.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/browser_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/code_analysis.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/common.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/definitions.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/file_reading.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/file_writing.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/find_tool.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/grep_tool.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/image_handler.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/mcp.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/repo_map.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/shell_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/todo_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/tool_schema.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/user_interaction.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/web_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal.egg-info/SOURCES.txt +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal.egg-info/dependency_links.txt +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal.egg-info/entry_points.txt +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal.egg-info/requires.txt +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/patchpal.egg-info/top_level.txt +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/pyproject.toml +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/setup.cfg +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_agent.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_cli.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_config_dynamic.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_context.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_custom_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_find_tool.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_guardrails.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_image_blocking.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_maximum_security.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_mcp_config.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_memory.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_memory_locations.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_operational_safety.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_optional_tools.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_permissions.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_react.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_reasoning_content.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_repo_map.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_simplified_prompt.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_skills.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_streaming.py +0 -0
- {patchpal-0.24.6 → patchpal-0.25.1}/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
|
|
@@ -106,6 +106,13 @@ def is_interactive_command(patchpal_args):
|
|
|
106
106
|
return False
|
|
107
107
|
|
|
108
108
|
# Check for autopilot
|
|
109
|
+
# NOTE: Only patchpal_args[0] is checked (not a scan of the whole list) by
|
|
110
|
+
# design. The "--" convention guarantees everything after it is passed to
|
|
111
|
+
# patchpal, and the autopilot subcommand, when present, is always the first
|
|
112
|
+
# of those args (e.g. `... -- autopilot --prompt-file task.md`). A later
|
|
113
|
+
# occurrence of the word "autopilot" would be a value/argument, not the
|
|
114
|
+
# subcommand, so it must not flip interactivity. Do not "fix" this into a
|
|
115
|
+
# full-list scan.
|
|
109
116
|
if patchpal_args[0] in ["autopilot", "patchpal-autopilot"]:
|
|
110
117
|
return False
|
|
111
118
|
|
|
@@ -124,6 +131,9 @@ def detect_llm_endpoints_from_env():
|
|
|
124
131
|
standard_endpoints = {
|
|
125
132
|
"OPENAI_API_KEY": "https://api.openai.com",
|
|
126
133
|
"ANTHROPIC_API_KEY": "https://api.anthropic.com",
|
|
134
|
+
# Google Gemini: LiteLLM commonly uses GEMINI_API_KEY; GOOGLE_API_KEY is
|
|
135
|
+
# also supported. Both map to the same generative language endpoint.
|
|
136
|
+
"GEMINI_API_KEY": "https://generativelanguage.googleapis.com",
|
|
127
137
|
"GOOGLE_API_KEY": "https://generativelanguage.googleapis.com",
|
|
128
138
|
"GROQ_API_KEY": "https://api.groq.com",
|
|
129
139
|
"COHERE_API_KEY": "https://api.cohere.ai",
|
|
@@ -574,6 +584,14 @@ def build_container_args(sandbox_args, patchpal_args, env_file_keys=None):
|
|
|
574
584
|
if sandbox_args.cpus:
|
|
575
585
|
container_args.extend(["--cpus", str(sandbox_args.cpus)])
|
|
576
586
|
|
|
587
|
+
# Forward user-specified volume mounts (-v/--volume) and environment
|
|
588
|
+
# variables (-e/--env). These are passed through verbatim to the container
|
|
589
|
+
# runtime, mirroring docker/podman's own -v and -e semantics.
|
|
590
|
+
for volume in getattr(sandbox_args, "volume", []) or []:
|
|
591
|
+
container_args.extend(["-v", volume])
|
|
592
|
+
for env_var in getattr(sandbox_args, "env", []) or []:
|
|
593
|
+
container_args.extend(["-e", env_var])
|
|
594
|
+
|
|
577
595
|
# Pass through environment variables.
|
|
578
596
|
# WITHOUT --env-file: pass through matching vars from the full host environment.
|
|
579
597
|
# WITH --env-file: isolate from the host, but still forward the vars that were
|
|
@@ -788,14 +806,48 @@ def build_container_args(sandbox_args, patchpal_args, env_file_keys=None):
|
|
|
788
806
|
|
|
789
807
|
# Add main patchpal command
|
|
790
808
|
if sandbox_args.restrict_network:
|
|
791
|
-
#
|
|
792
|
-
#
|
|
809
|
+
# Pass through all patchpal args via "$@" (identical to normal mode) so that
|
|
810
|
+
# the autopilot subcommand and flags like --prompt-file / --max-iterations are
|
|
811
|
+
# preserved. The model is included in "$@" when --model was given on the command
|
|
812
|
+
# line. If no --model was provided, fall back to the MODEL/PATCHPAL_MODEL env var
|
|
813
|
+
# (launch-agent-sandbox pattern).
|
|
814
|
+
#
|
|
815
|
+
# SECURITY: Drop CAP_NET_ADMIN / CAP_NET_RAW before handing control to the
|
|
816
|
+
# agent. The container runs as root (UID 0) and keeps the capability that
|
|
817
|
+
# was used to build the iptables firewall. Without dropping it, an agent
|
|
818
|
+
# that can execute shell commands could simply run `iptables -F OUTPUT`
|
|
819
|
+
# (or `iptables -P OUTPUT ACCEPT`) and dismantle the very network
|
|
820
|
+
# restrictions that --restrict-network is supposed to enforce.
|
|
821
|
+
#
|
|
822
|
+
# We drop the capabilities from BOTH the inheritable and bounding sets via
|
|
823
|
+
# setpriv. For a root process this also clears them from the effective and
|
|
824
|
+
# permitted sets, and removing them from the bounding set means they cannot
|
|
825
|
+
# be re-raised afterwards. The firewall rules remain in force, but the
|
|
826
|
+
# agent process can no longer modify netfilter. (setpriv ships with
|
|
827
|
+
# util-linux and is present in the sandbox image; if it is somehow missing
|
|
828
|
+
# we fall back to a plain exec and warn loudly.)
|
|
829
|
+
if model_arg:
|
|
830
|
+
# --model is already present in the pass-through args
|
|
831
|
+
model_display = model_arg
|
|
832
|
+
run_cmd = f'{patchpal_cmd} "$@"'
|
|
833
|
+
else:
|
|
834
|
+
# No --model on command line; supply it from the env var fallback
|
|
835
|
+
model_display = "${MODEL:-$PATCHPAL_MODEL}"
|
|
836
|
+
run_cmd = f'{patchpal_cmd} --model "${{MODEL:-$PATCHPAL_MODEL}}" "$@"'
|
|
793
837
|
shell_cmd_parts.append(f"""
|
|
794
838
|
echo ""
|
|
795
839
|
echo "=== Starting PatchPal ==="
|
|
796
|
-
echo "Model:
|
|
840
|
+
echo "Model: {model_display}"
|
|
797
841
|
echo ""
|
|
798
|
-
|
|
842
|
+
if command -v setpriv >/dev/null 2>&1; then
|
|
843
|
+
echo "Dropping CAP_NET_ADMIN/CAP_NET_RAW before starting agent (firewall now tamper-resistant)"
|
|
844
|
+
exec setpriv --inh-caps -net_admin,-net_raw --bounding-set -net_admin,-net_raw \\
|
|
845
|
+
{run_cmd}
|
|
846
|
+
else
|
|
847
|
+
echo "⚠️ WARNING: setpriv not found - cannot drop network capabilities."
|
|
848
|
+
echo "⚠️ The agent runs as root and could tamper with the iptables firewall."
|
|
849
|
+
exec {run_cmd}
|
|
850
|
+
fi
|
|
799
851
|
""")
|
|
800
852
|
else:
|
|
801
853
|
# Normal mode: use $@ to pass all arguments
|
|
@@ -813,13 +865,9 @@ exec {patchpal_cmd} "$@"
|
|
|
813
865
|
# Add set -e at the beginning to exit on any error
|
|
814
866
|
shell_cmd = "set -e\n" + "\n".join(shell_cmd_parts)
|
|
815
867
|
|
|
816
|
-
# Pass
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
container_args.extend(["bash", "-c", shell_cmd])
|
|
820
|
-
else:
|
|
821
|
-
# Without restrictions: pass args after -- separator
|
|
822
|
-
container_args.extend(["bash", "-c", shell_cmd, "--"] + patchpal_cmd_args)
|
|
868
|
+
# Pass patchpal args after the "--" separator so they populate "$@" in the
|
|
869
|
+
# bash script. This applies to both restricted and unrestricted modes.
|
|
870
|
+
container_args.extend(["bash", "-c", shell_cmd, "--"] + patchpal_cmd_args)
|
|
823
871
|
|
|
824
872
|
return container_args, runtime
|
|
825
873
|
|
|
@@ -853,14 +901,20 @@ SCRIPT OPTIONS:
|
|
|
853
901
|
--host-network Use host network (for local Ollama/vLLM servers on localhost)
|
|
854
902
|
--memory LIMIT Memory limit (e.g., 2g, 4g) - optional, no limit by default
|
|
855
903
|
--cpus NUM CPU limit (e.g., 2, 4) - optional, no limit by default
|
|
856
|
-
--env-file FILE Load environment variables from .env file
|
|
904
|
+
--env-file FILE Load environment variables from .env file (single file)
|
|
905
|
+
-v, --volume HOST:CONTAINER[:ro]
|
|
906
|
+
Mount an additional volume (can be specified multiple times)
|
|
907
|
+
Example: -v /path/to/data:/data:ro
|
|
908
|
+
-e, --env KEY=VALUE Pass an additional environment variable into the container
|
|
909
|
+
(can be specified multiple times)
|
|
910
|
+
Example: -e MY_VAR=value
|
|
857
911
|
--restrict-network Enable iptables firewall for network isolation
|
|
858
912
|
Automatically detects LLM endpoints from environment variables:
|
|
859
913
|
* OpenAI (OPENAI_API_KEY)
|
|
860
914
|
* Anthropic (ANTHROPIC_API_KEY)
|
|
861
915
|
* AWS Bedrock (AWS_REGION + AWS_ACCESS_KEY_ID)
|
|
862
916
|
* Azure OpenAI (AZURE_OPENAI_KEY + AZURE_OPENAI_RESOURCE)
|
|
863
|
-
* Google AI (GOOGLE_API_KEY)
|
|
917
|
+
* Google AI (GEMINI_API_KEY or GOOGLE_API_KEY)
|
|
864
918
|
* Groq, Cohere, Together, Replicate, etc.
|
|
865
919
|
Custom endpoints also detected from *_BASE_URL env vars
|
|
866
920
|
--allow-url URL Allow network access to specific URL (can be specified multiple times)
|
|
@@ -1126,6 +1180,24 @@ def main():
|
|
|
1126
1180
|
action="store_true",
|
|
1127
1181
|
help="Drop into bash shell instead of running patchpal (for debugging)",
|
|
1128
1182
|
)
|
|
1183
|
+
parser.add_argument(
|
|
1184
|
+
"-v",
|
|
1185
|
+
"--volume",
|
|
1186
|
+
action="append",
|
|
1187
|
+
default=[],
|
|
1188
|
+
metavar="HOST:CONTAINER[:ro]",
|
|
1189
|
+
help="Mount an additional volume into the container (can be specified multiple times). "
|
|
1190
|
+
"Example: -v /path/to/data:/data:ro",
|
|
1191
|
+
)
|
|
1192
|
+
parser.add_argument(
|
|
1193
|
+
"-e",
|
|
1194
|
+
"--env",
|
|
1195
|
+
action="append",
|
|
1196
|
+
default=[],
|
|
1197
|
+
metavar="KEY=VALUE",
|
|
1198
|
+
help="Pass an additional environment variable into the container "
|
|
1199
|
+
"(can be specified multiple times). Example: -e MY_VAR=value",
|
|
1200
|
+
)
|
|
1129
1201
|
|
|
1130
1202
|
try:
|
|
1131
1203
|
sandbox_args = parser.parse_args(sandbox_argv)
|
|
@@ -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
|