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.
Files changed (70) hide show
  1. {patchpal-0.25.0/patchpal.egg-info → patchpal-0.25.2}/PKG-INFO +1 -1
  2. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/__init__.py +1 -1
  3. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/agent/function_calling.py +27 -3
  4. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/sandbox.py +85 -13
  5. {patchpal-0.25.0 → patchpal-0.25.2/patchpal.egg-info}/PKG-INFO +1 -1
  6. {patchpal-0.25.0 → patchpal-0.25.2}/LICENSE +0 -0
  7. {patchpal-0.25.0 → patchpal-0.25.2}/MANIFEST.in +0 -0
  8. {patchpal-0.25.0 → patchpal-0.25.2}/README.md +0 -0
  9. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/agent/__init__.py +0 -0
  10. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/agent/bedrock_profile_utils.py +0 -0
  11. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/agent/react.py +0 -0
  12. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/__init__.py +0 -0
  13. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/autopilot.py +0 -0
  14. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/interactive.py +0 -0
  15. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/mcp.py +0 -0
  16. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/cli/streaming.py +0 -0
  17. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/config.py +0 -0
  18. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/context.py +0 -0
  19. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/permissions.py +0 -0
  20. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/prompts/react_prompt.md +0 -0
  21. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/prompts/system_prompt.md +0 -0
  22. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/skills.py +0 -0
  23. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/__init__.py +0 -0
  24. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/audit.py +0 -0
  25. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/browser_tools.py +0 -0
  26. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/code_analysis.py +0 -0
  27. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/common.py +0 -0
  28. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/definitions.py +0 -0
  29. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/file_reading.py +0 -0
  30. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/file_writing.py +0 -0
  31. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/find_tool.py +0 -0
  32. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/grep_tool.py +0 -0
  33. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/image_handler.py +0 -0
  34. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/mcp.py +0 -0
  35. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/repo_map.py +0 -0
  36. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/shell_tools.py +0 -0
  37. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/todo_tools.py +0 -0
  38. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/tool_schema.py +0 -0
  39. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/user_interaction.py +0 -0
  40. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal/tools/web_tools.py +0 -0
  41. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal.egg-info/SOURCES.txt +0 -0
  42. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal.egg-info/dependency_links.txt +0 -0
  43. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal.egg-info/entry_points.txt +0 -0
  44. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal.egg-info/requires.txt +0 -0
  45. {patchpal-0.25.0 → patchpal-0.25.2}/patchpal.egg-info/top_level.txt +0 -0
  46. {patchpal-0.25.0 → patchpal-0.25.2}/pyproject.toml +0 -0
  47. {patchpal-0.25.0 → patchpal-0.25.2}/setup.cfg +0 -0
  48. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_agent.py +0 -0
  49. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_cli.py +0 -0
  50. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_config_dynamic.py +0 -0
  51. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_context.py +0 -0
  52. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_custom_tools.py +0 -0
  53. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_enabled_tools.py +0 -0
  54. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_find_tool.py +0 -0
  55. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_guardrails.py +0 -0
  56. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_image_blocking.py +0 -0
  57. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_maximum_security.py +0 -0
  58. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_mcp_config.py +0 -0
  59. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_memory.py +0 -0
  60. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_memory_locations.py +0 -0
  61. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_operational_safety.py +0 -0
  62. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_optional_tools.py +0 -0
  63. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_permissions.py +0 -0
  64. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_react.py +0 -0
  65. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_reasoning_content.py +0 -0
  66. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_repo_map.py +0 -0
  67. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_simplified_prompt.py +0 -0
  68. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_skills.py +0 -0
  69. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_streaming.py +0 -0
  70. {patchpal-0.25.0 → patchpal-0.25.2}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.25.0
3
+ Version: 0.25.2
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.25.0"
3
+ __version__ = "0.25.2"
4
4
 
5
5
  from patchpal.agent import create_agent, create_react_agent
6
6
  from patchpal.cli.autopilot import autopilot_loop
@@ -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 can happen with some local models (e.g., GPT-OSS-120B)
1839
- # Using "NO_RESPONSE:" prefix for programmatic detection
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
- # Use launch-agent-sandbox pattern: model from env var
792
- # Falls back to PATCHPAL_MODEL if MODEL not set
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: ${{MODEL:-$PATCHPAL_MODEL}}"
840
+ echo "Model: {model_display}"
797
841
  echo ""
798
- exec {patchpal_cmd} --model "${{MODEL:-$PATCHPAL_MODEL}}"
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 arguments differently based on network restrictions
817
- if sandbox_args.restrict_network:
818
- # With restrictions: model passed as env var, no additional args needed
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.25.0
3
+ Version: 0.25.2
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
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