patchpal 0.24.0__tar.gz → 0.24.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.
Files changed (70) hide show
  1. {patchpal-0.24.0/patchpal.egg-info → patchpal-0.24.1}/PKG-INFO +1 -1
  2. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/__init__.py +1 -1
  3. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/cli/sandbox.py +38 -7
  4. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/shell_tools.py +5 -1
  5. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/web_tools.py +14 -2
  6. {patchpal-0.24.0 → patchpal-0.24.1/patchpal.egg-info}/PKG-INFO +1 -1
  7. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_permissions.py +28 -0
  8. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_tools.py +38 -0
  9. {patchpal-0.24.0 → patchpal-0.24.1}/LICENSE +0 -0
  10. {patchpal-0.24.0 → patchpal-0.24.1}/MANIFEST.in +0 -0
  11. {patchpal-0.24.0 → patchpal-0.24.1}/README.md +0 -0
  12. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/agent/__init__.py +0 -0
  13. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/agent/bedrock_profile_utils.py +0 -0
  14. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/agent/function_calling.py +0 -0
  15. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/agent/react.py +0 -0
  16. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/cli/__init__.py +0 -0
  17. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/cli/autopilot.py +0 -0
  18. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/cli/interactive.py +0 -0
  19. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/cli/mcp.py +0 -0
  20. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/cli/streaming.py +0 -0
  21. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/config.py +0 -0
  22. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/context.py +0 -0
  23. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/permissions.py +0 -0
  24. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/prompts/react_prompt.md +0 -0
  25. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/prompts/system_prompt.md +0 -0
  26. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/skills.py +0 -0
  27. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/__init__.py +0 -0
  28. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/audit.py +0 -0
  29. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/browser_tools.py +0 -0
  30. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/code_analysis.py +0 -0
  31. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/common.py +0 -0
  32. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/definitions.py +0 -0
  33. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/file_reading.py +0 -0
  34. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/file_writing.py +0 -0
  35. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/find_tool.py +0 -0
  36. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/grep_tool.py +0 -0
  37. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/image_handler.py +0 -0
  38. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/mcp.py +0 -0
  39. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/repo_map.py +0 -0
  40. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/todo_tools.py +0 -0
  41. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/tool_schema.py +0 -0
  42. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal/tools/user_interaction.py +0 -0
  43. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal.egg-info/SOURCES.txt +0 -0
  44. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal.egg-info/dependency_links.txt +0 -0
  45. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal.egg-info/entry_points.txt +0 -0
  46. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal.egg-info/requires.txt +0 -0
  47. {patchpal-0.24.0 → patchpal-0.24.1}/patchpal.egg-info/top_level.txt +0 -0
  48. {patchpal-0.24.0 → patchpal-0.24.1}/pyproject.toml +0 -0
  49. {patchpal-0.24.0 → patchpal-0.24.1}/setup.cfg +0 -0
  50. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_agent.py +0 -0
  51. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_cli.py +0 -0
  52. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_config_dynamic.py +0 -0
  53. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_context.py +0 -0
  54. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_custom_tools.py +0 -0
  55. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_enabled_tools.py +0 -0
  56. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_find_tool.py +0 -0
  57. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_guardrails.py +0 -0
  58. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_image_blocking.py +0 -0
  59. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_maximum_security.py +0 -0
  60. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_mcp_config.py +0 -0
  61. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_memory.py +0 -0
  62. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_memory_locations.py +0 -0
  63. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_operational_safety.py +0 -0
  64. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_optional_tools.py +0 -0
  65. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_react.py +0 -0
  66. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_reasoning_content.py +0 -0
  67. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_repo_map.py +0 -0
  68. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_simplified_prompt.py +0 -0
  69. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_skills.py +0 -0
  70. {patchpal-0.24.0 → patchpal-0.24.1}/tests/test_streaming.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.24.0
3
+ Version: 0.24.1
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.0"
3
+ __version__ = "0.24.1"
4
4
 
5
5
  from patchpal.agent import create_agent, create_react_agent
6
6
  from patchpal.cli.autopilot import autopilot_loop
@@ -18,11 +18,17 @@ import sys
18
18
 
19
19
 
20
20
  def load_env_file(env_file):
21
- """Load environment variables from .env file."""
21
+ """Load environment variables from .env file.
22
+
23
+ Returns:
24
+ set: Keys of the environment variables that were loaded from the file.
25
+ """
22
26
  if not os.path.exists(env_file):
23
27
  print(f"❌ Error: .env file not found: {env_file}", file=sys.stderr)
24
28
  sys.exit(1)
25
29
 
30
+ loaded_keys = set()
31
+
26
32
  with open(env_file, "r") as f:
27
33
  for line in f:
28
34
  line = line.strip()
@@ -43,6 +49,9 @@ def load_env_file(env_file):
43
49
  value = value[1:-1]
44
50
 
45
51
  os.environ[key] = value
52
+ loaded_keys.add(key)
53
+
54
+ return loaded_keys
46
55
 
47
56
 
48
57
  def detect_runtime():
@@ -354,8 +363,17 @@ echo ""
354
363
  return script
355
364
 
356
365
 
357
- def build_container_args(sandbox_args, patchpal_args):
358
- """Build container runtime arguments."""
366
+ def build_container_args(sandbox_args, patchpal_args, env_file_keys=None):
367
+ """Build container runtime arguments.
368
+
369
+ Args:
370
+ sandbox_args: Parsed sandbox CLI arguments
371
+ patchpal_args: Arguments to pass through to patchpal
372
+ env_file_keys: Set of env var names loaded from --env-file (if any).
373
+ These are forwarded into the container even though the
374
+ rest of the host environment is not (isolation mode).
375
+ """
376
+ env_file_keys = env_file_keys or set()
359
377
  runtime = detect_runtime()
360
378
  if not runtime:
361
379
  print("❌ Error: Neither Docker nor Podman found", file=sys.stderr)
@@ -456,8 +474,12 @@ def build_container_args(sandbox_args, patchpal_args):
456
474
  if sandbox_args.cpus:
457
475
  container_args.extend(["--cpus", str(sandbox_args.cpus)])
458
476
 
459
- # Pass through environment variables from host ONLY if --env-file is NOT provided
460
- # This provides better isolation - when using --env-file, only those specific vars are used
477
+ # Pass through environment variables.
478
+ # WITHOUT --env-file: pass through matching vars from the full host environment.
479
+ # WITH --env-file: isolate from the host, but still forward the vars that were
480
+ # explicitly loaded from the file itself (previously these were
481
+ # loaded into os.environ but never forwarded into the container,
482
+ # causing credentials in --env-file to be silently dropped).
461
483
  if not sandbox_args.env_file:
462
484
  # Pass through PATCHPAL_* environment variables
463
485
  for key, value in os.environ.items():
@@ -496,6 +518,12 @@ def build_container_args(sandbox_args, patchpal_args):
496
518
  for key, value in os.environ.items():
497
519
  if key.startswith("OLLAMA_"):
498
520
  container_args.extend(["-e", f"{key}={value}"])
521
+ else:
522
+ # Isolated mode: only forward the specific vars loaded from --env-file
523
+ for key in env_file_keys:
524
+ value = os.environ.get(key)
525
+ if value is not None:
526
+ container_args.extend(["-e", f"{key}={value}"])
499
527
 
500
528
  # Track mounted paths to avoid duplicates
501
529
  mounted_paths = set()
@@ -1002,9 +1030,10 @@ def main():
1002
1030
  sandbox_args.network = "host"
1003
1031
 
1004
1032
  # Load .env file if specified
1033
+ env_file_keys = set()
1005
1034
  if sandbox_args.env_file:
1006
1035
  print(f"Loading environment variables from: {sandbox_args.env_file}")
1007
- load_env_file(sandbox_args.env_file)
1036
+ env_file_keys = load_env_file(sandbox_args.env_file)
1008
1037
 
1009
1038
  # If LITELLM_KWARGS contains AWS params, normalize env var names to uppercase
1010
1039
  # This must happen before endpoint detection for AWS Bedrock auto-detection to work
@@ -1023,6 +1052,7 @@ def main():
1023
1052
  upper_key = key.upper()
1024
1053
  if upper_key not in os.environ:
1025
1054
  os.environ[upper_key] = value
1055
+ env_file_keys.add(upper_key)
1026
1056
  except (json.JSONDecodeError, ValueError):
1027
1057
  # Not JSON, check if individual env vars exist (comma-separated format)
1028
1058
  for key, value in list(os.environ.items()):
@@ -1030,6 +1060,7 @@ def main():
1030
1060
  upper_key = key.upper()
1031
1061
  if upper_key not in os.environ:
1032
1062
  os.environ[upper_key] = value
1063
+ env_file_keys.add(upper_key)
1033
1064
 
1034
1065
  # Auto-detect LLM endpoints if network restrictions are enabled
1035
1066
  detected_endpoints = []
@@ -1060,7 +1091,7 @@ def main():
1060
1091
  )
1061
1092
 
1062
1093
  # Build container command
1063
- container_args, runtime = build_container_args(sandbox_args, patchpal_argv)
1094
+ container_args, runtime = build_container_args(sandbox_args, patchpal_argv, env_file_keys)
1064
1095
 
1065
1096
  # Show what we're doing
1066
1097
  print(f"Using container runtime: {runtime}")
@@ -169,7 +169,11 @@ def _extract_shell_command_info(cmd: str) -> tuple[Optional[str], Optional[str]]
169
169
 
170
170
  # Shell operators that indicate compound commands
171
171
  # Split by && and || first (they group tighter than ;)
172
- compound_operators = ["&&", "||", ";"]
172
+ # IMPORTANT: newline is also a command separator - without splitting on it, a
173
+ # multi-line command like "cd /tmp\npython evil.py" would be tokenized as one
174
+ # blob and misidentified as just "cd" (which is auto-granted as harmless),
175
+ # completely bypassing the permission check for the real command on the second line.
176
+ compound_operators = ["&&", "||", ";", "\n"]
173
177
 
174
178
  # Split by compound operators to find all sub-commands
175
179
  commands = [cmd]
@@ -289,9 +289,21 @@ class URLContextTracker:
289
289
  # Note: Apostrophes (') are allowed in URLs (e.g., Wikipedia article titles)
290
290
  import re
291
291
 
292
- url_pattern = r"https?://[^\s<>\"\)]*"
292
+ url_pattern = r"https?://[^\s<>\"]*"
293
293
  urls = re.findall(url_pattern, text)
294
- self.seen_urls.update(urls)
294
+
295
+ # Strip trailing punctuation that commonly follows URLs in natural language
296
+ # but is not part of the URL itself
297
+ cleaned_urls = []
298
+ for url in urls:
299
+ # Strip trailing punctuation: , ; . ! ? )
300
+ # but preserve these characters if they appear mid-URL
301
+ while url and url[-1] in ".,;!?)":
302
+ url = url[:-1]
303
+ if url: # Only add non-empty URLs
304
+ cleaned_urls.append(url)
305
+
306
+ self.seen_urls.update(cleaned_urls)
295
307
 
296
308
  def is_url_in_context(self, url: str) -> bool:
297
309
  """Check if URL has appeared in conversation context.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.24.0
3
+ Version: 0.24.1
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
@@ -48,6 +48,34 @@ def test_shell_command_pattern_with_cd():
48
48
  assert wd == "/tmp"
49
49
 
50
50
 
51
+ def test_shell_command_pattern_newline_separated():
52
+ """Regression test: newline-separated commands must not bypass permission checks.
53
+
54
+ Bug: _extract_shell_command_info() only split on &&, ||, and ; - not newlines.
55
+ A command like "cd /tmp\npython evil.py" (cd and the real command on separate
56
+ lines, with no shell operator joining them) was tokenized as a single blob and
57
+ misidentified as just "cd", which is on the harmless auto-grant list. This let
58
+ an arbitrary second-line command execute without ever triggering a permission
59
+ prompt. See conversation history for full root-cause analysis.
60
+ """
61
+ from patchpal.tools.shell_tools import _extract_shell_command_info
62
+
63
+ # Basic two-line case: cd on line 1, real command on line 2
64
+ cmd, wd = _extract_shell_command_info("cd /tmp\npython evil.py")
65
+ assert cmd == "python" # Must NOT be "cd"
66
+ assert wd == "/tmp"
67
+
68
+ # Multi-line with || fallback on line 1 (the exact bypass pattern reported)
69
+ cmd, wd = _extract_shell_command_info(
70
+ 'cd /repo/a 2>/dev/null || cd /repo/b\npython3 -c "print(1)"'
71
+ )
72
+ assert cmd == "python3" # Must NOT be "cd"
73
+
74
+ # Newline-separated dangerous command must also be detected (not swallowed as "cd")
75
+ cmd, wd = _extract_shell_command_info("cd /tmp\nrm -rf /")
76
+ assert cmd == "rm" # Must NOT be "cd" - "rm" must surface so DANGEROUS_PATTERNS can catch it
77
+
78
+
51
79
  def test_shell_command_pattern_with_pipes():
52
80
  """Test shell command pattern extraction with pipes."""
53
81
  from patchpal.tools.shell_tools import _extract_shell_command_info
@@ -679,6 +679,44 @@ def test_web_fetch_invalid_url():
679
679
  web_fetch("not-a-url")
680
680
 
681
681
 
682
+ def test_url_extraction_with_punctuation():
683
+ """Test that URLs followed by punctuation are extracted correctly."""
684
+ from patchpal.tools.web_tools import URLContextTracker
685
+
686
+ tracker = URLContextTracker()
687
+
688
+ # Test URL followed by comma (the bug that was fixed)
689
+ tracker.clear()
690
+ tracker.add_urls_from_text("Based on https://en.wikipedia.org/robots.txt, is this okay?")
691
+ assert tracker.is_url_in_context("https://en.wikipedia.org/robots.txt")
692
+
693
+ # Test URL followed by period
694
+ tracker.clear()
695
+ tracker.add_urls_from_text("Check out https://example.com. It's great!")
696
+ assert tracker.is_url_in_context("https://example.com")
697
+
698
+ # Test URL followed by semicolon
699
+ tracker.clear()
700
+ tracker.add_urls_from_text("Visit https://docs.python.org; it has good info.")
701
+ assert tracker.is_url_in_context("https://docs.python.org")
702
+
703
+ # Test URL in parentheses
704
+ tracker.clear()
705
+ tracker.add_urls_from_text("Link (https://www.google.com) is here.")
706
+ assert tracker.is_url_in_context("https://www.google.com")
707
+
708
+ # Test multiple URLs with different punctuation
709
+ tracker.clear()
710
+ tracker.add_urls_from_text("Multiple: https://site1.com, https://site2.com.")
711
+ assert tracker.is_url_in_context("https://site1.com")
712
+ assert tracker.is_url_in_context("https://site2.com")
713
+
714
+ # Test URL with apostrophe (should be preserved - part of URL)
715
+ tracker.clear()
716
+ tracker.add_urls_from_text("URL: https://en.wikipedia.org/wiki/O'Brien works")
717
+ assert tracker.is_url_in_context("https://en.wikipedia.org/wiki/O'Brien")
718
+
719
+
682
720
  def test_web_fetch_content_too_large(monkeypatch):
683
721
  """Test that large content is rejected."""
684
722
  from unittest.mock import Mock
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