patchpal 0.25.0__tar.gz → 0.25.2__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.25.0/patchpal.egg-info → patchpal-0.25.2}/PKG-INFO +1 -1
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/__init__.py +1 -1
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/agent/function_calling.py +27 -3
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/sandbox.py +85 -13
- {patchpal-0.25.0 → patchpal-0.25.2/patchpal.egg-info}/PKG-INFO +1 -1
- {patchpal-0.25.0 → patchpal-0.25.2}/LICENSE +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/MANIFEST.in +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/README.md +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/agent/__init__.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/agent/bedrock_profile_utils.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/agent/react.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/__init__.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/autopilot.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/interactive.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/mcp.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/streaming.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/config.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/context.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/permissions.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/prompts/react_prompt.md +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/prompts/system_prompt.md +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/skills.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/__init__.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/audit.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/browser_tools.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/code_analysis.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/common.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/definitions.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/file_reading.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/file_writing.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/find_tool.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/grep_tool.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/image_handler.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/mcp.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/repo_map.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/shell_tools.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/todo_tools.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/tool_schema.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/user_interaction.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/web_tools.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal.egg-info/SOURCES.txt +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal.egg-info/dependency_links.txt +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal.egg-info/entry_points.txt +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal.egg-info/requires.txt +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/patchpal.egg-info/top_level.txt +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/pyproject.toml +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/setup.cfg +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_agent.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_cli.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_config_dynamic.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_context.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_custom_tools.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_enabled_tools.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_find_tool.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_guardrails.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_image_blocking.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_maximum_security.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_mcp_config.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_memory.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_memory_locations.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_operational_safety.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_optional_tools.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_permissions.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_react.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_reasoning_content.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_repo_map.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_simplified_prompt.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_skills.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_streaming.py +0 -0
- {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_tools.py +0 -0
|
@@ -1161,6 +1161,12 @@ Repository root takes priority if both exist."""
|
|
|
1161
1161
|
The agent's final response
|
|
1162
1162
|
"""
|
|
1163
1163
|
# Agent loop
|
|
1164
|
+
# Track consecutive empty responses (no content + no tool calls).
|
|
1165
|
+
# Common with some local models (e.g., GPT-OSS-120B on vLLM) due to
|
|
1166
|
+
# intermittent harmony-format parsing hiccups. A bounded retry usually
|
|
1167
|
+
# recovers since the failure is probabilistic.
|
|
1168
|
+
empty_response_retries = 0
|
|
1169
|
+
empty_response_max_retries = int(os.environ.get("PATCHPAL_EMPTY_RETRY_MAX", "2"))
|
|
1164
1170
|
for iteration in range(max_iterations):
|
|
1165
1171
|
# Clear any stale pending images from previous iteration (safety check)
|
|
1166
1172
|
# Images should already be injected, but this prevents accumulation on error
|
|
@@ -1335,6 +1341,8 @@ Repository root takes priority if both exist."""
|
|
|
1335
1341
|
|
|
1336
1342
|
# Check if there are tool calls
|
|
1337
1343
|
if hasattr(assistant_message, "tool_calls") and assistant_message.tool_calls:
|
|
1344
|
+
# A non-empty turn succeeded; reset the empty-response retry budget
|
|
1345
|
+
empty_response_retries = 0
|
|
1338
1346
|
# Print explanation text before executing tools (render as markdown)
|
|
1339
1347
|
if assistant_message.content and assistant_message.content.strip():
|
|
1340
1348
|
console = Console()
|
|
@@ -1834,9 +1842,25 @@ Repository root takes priority if both exist."""
|
|
|
1834
1842
|
if assistant_message.content:
|
|
1835
1843
|
return assistant_message.content
|
|
1836
1844
|
else:
|
|
1837
|
-
# Model returned no content and no tool calls
|
|
1838
|
-
# This
|
|
1839
|
-
#
|
|
1845
|
+
# Model returned no content and no tool calls.
|
|
1846
|
+
# This happens intermittently with some local models
|
|
1847
|
+
# (e.g., GPT-OSS-120B on vLLM) due to harmony-format parsing
|
|
1848
|
+
# hiccups. Because the failure is probabilistic, retry the turn
|
|
1849
|
+
# a bounded number of times before giving up.
|
|
1850
|
+
if empty_response_retries < empty_response_max_retries:
|
|
1851
|
+
empty_response_retries += 1
|
|
1852
|
+
# Drop the empty assistant message so it doesn't pollute history
|
|
1853
|
+
if self.messages and self.messages[-1] is assistant_msg:
|
|
1854
|
+
self.messages.pop()
|
|
1855
|
+
print(
|
|
1856
|
+
f"\033[2m↻ Empty response from model; retrying "
|
|
1857
|
+
f"({empty_response_retries}/{empty_response_max_retries})...\033[0m",
|
|
1858
|
+
flush=True,
|
|
1859
|
+
)
|
|
1860
|
+
continue
|
|
1861
|
+
|
|
1862
|
+
# Retries exhausted: surface the honest failure.
|
|
1863
|
+
# Using "NO_RESPONSE:" prefix for programmatic detection.
|
|
1840
1864
|
return "NO_RESPONSE: Model returned empty response. Try rephrasing your request or type 'continue' to prompt again."
|
|
1841
1865
|
|
|
1842
1866
|
# Max iterations reached
|
|
@@ -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)
|
|
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
|