patchpal 0.23.2__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.23.2/patchpal.egg-info → patchpal-0.24.1}/PKG-INFO +6 -1
  2. {patchpal-0.23.2 → patchpal-0.24.1}/README.md +2 -0
  3. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/__init__.py +1 -1
  4. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/agent/function_calling.py +86 -0
  5. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/cli/interactive.py +45 -1
  6. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/cli/sandbox.py +38 -7
  7. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/config.py +10 -0
  8. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/permissions.py +17 -0
  9. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/__init__.py +52 -0
  10. patchpal-0.24.1/patchpal/tools/browser_tools.py +1171 -0
  11. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/definitions.py +275 -0
  12. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/shell_tools.py +5 -1
  13. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/web_tools.py +71 -17
  14. {patchpal-0.23.2 → patchpal-0.24.1/patchpal.egg-info}/PKG-INFO +6 -1
  15. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal.egg-info/SOURCES.txt +1 -0
  16. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal.egg-info/requires.txt +4 -0
  17. {patchpal-0.23.2 → patchpal-0.24.1}/pyproject.toml +4 -0
  18. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_agent.py +40 -6
  19. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_permissions.py +28 -0
  20. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_tools.py +38 -0
  21. {patchpal-0.23.2 → patchpal-0.24.1}/LICENSE +0 -0
  22. {patchpal-0.23.2 → patchpal-0.24.1}/MANIFEST.in +0 -0
  23. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/agent/__init__.py +0 -0
  24. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/agent/bedrock_profile_utils.py +0 -0
  25. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/agent/react.py +0 -0
  26. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/cli/__init__.py +0 -0
  27. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/cli/autopilot.py +0 -0
  28. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/cli/mcp.py +0 -0
  29. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/cli/streaming.py +0 -0
  30. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/context.py +0 -0
  31. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/prompts/react_prompt.md +0 -0
  32. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/prompts/system_prompt.md +0 -0
  33. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/skills.py +0 -0
  34. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/audit.py +0 -0
  35. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/code_analysis.py +0 -0
  36. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/common.py +0 -0
  37. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/file_reading.py +0 -0
  38. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/file_writing.py +0 -0
  39. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/find_tool.py +0 -0
  40. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/grep_tool.py +0 -0
  41. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/image_handler.py +0 -0
  42. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/mcp.py +0 -0
  43. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/repo_map.py +0 -0
  44. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/todo_tools.py +0 -0
  45. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/tool_schema.py +0 -0
  46. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal/tools/user_interaction.py +0 -0
  47. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal.egg-info/dependency_links.txt +0 -0
  48. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal.egg-info/entry_points.txt +0 -0
  49. {patchpal-0.23.2 → patchpal-0.24.1}/patchpal.egg-info/top_level.txt +0 -0
  50. {patchpal-0.23.2 → patchpal-0.24.1}/setup.cfg +0 -0
  51. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_cli.py +0 -0
  52. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_config_dynamic.py +0 -0
  53. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_context.py +0 -0
  54. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_custom_tools.py +0 -0
  55. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_enabled_tools.py +0 -0
  56. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_find_tool.py +0 -0
  57. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_guardrails.py +0 -0
  58. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_image_blocking.py +0 -0
  59. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_maximum_security.py +0 -0
  60. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_mcp_config.py +0 -0
  61. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_memory.py +0 -0
  62. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_memory_locations.py +0 -0
  63. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_operational_safety.py +0 -0
  64. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_optional_tools.py +0 -0
  65. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_react.py +0 -0
  66. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_reasoning_content.py +0 -0
  67. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_repo_map.py +0 -0
  68. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_simplified_prompt.py +0 -0
  69. {patchpal-0.23.2 → patchpal-0.24.1}/tests/test_skills.py +0 -0
  70. {patchpal-0.23.2 → 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.23.2
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
@@ -38,6 +38,9 @@ Requires-Dist: ruff==0.14.13; extra == "dev"
38
38
  Requires-Dist: pre-commit>=3.0.0; extra == "dev"
39
39
  Provides-Extra: mcp
40
40
  Requires-Dist: mcp>=0.9.0; extra == "mcp"
41
+ Provides-Extra: browser
42
+ Requires-Dist: playwright>=1.40.0; extra == "browser"
43
+ Requires-Dist: nest-asyncio>=1.5.0; extra == "browser"
41
44
  Provides-Extra: docs
42
45
  Requires-Dist: mkdocs<2.0,>=1.5.0; extra == "docs"
43
46
  Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
@@ -62,6 +65,7 @@ Most agent frameworks are [built in TypeScript](https://news.ycombinator.com/ite
62
65
  - [Sandbox Mode](https://amaiya.github.io/patchpal/usage/sandbox/) for secure container isolation
63
66
  - [Python SDK](https://amaiya.github.io/patchpal/usage/python-api/) for flexibility and extensibility
64
67
  - [Built-In](https://amaiya.github.io/patchpal/features/tools/) and [Custom Tools](https://amaiya.github.io/patchpal/features/custom-tools/)
68
+ - [Browser Automation](https://amaiya.github.io/patchpal/features/tools/#browser-automation-14-tools-optional) for interactive web tasks, form filling, and JavaScript-heavy sites
65
69
  - [Skills System](https://amaiya.github.io/patchpal/features/skills/) and [MCP Integration](https://amaiya.github.io/patchpal/features/mcp/)
66
70
  - [Autopilot Mode](https://amaiya.github.io/patchpal/usage/autopilot/) using [Ralph Wiggum loops](https://github.com/amaiya/patchpal/tree/main/examples/ralph/)
67
71
  - [Project Memory](https://amaiya.github.io/patchpal/features/memory/) automatically loads project context from `MEMORY.md` (repository root or `~/.patchpal/repos/<repo-name>/MEMORY.md`) at startup.
@@ -108,6 +112,7 @@ docker run -it --rm \
108
112
 
109
113
  ## Setup
110
114
  0. **Install**: `pip install patchpal`
115
+ - **Optional**: For browser automation (13 additional tools): `pip install patchpal[browser] && python -m playwright install chromium`
111
116
  1. **Get an API key or a Local LLM Engine**:
112
117
  - **[Cloud]** For Anthropic models (default): Sign up at https://console.anthropic.com/
113
118
  - **[Cloud]** For OpenAI models: Get a key from https://platform.openai.com/
@@ -14,6 +14,7 @@ Most agent frameworks are [built in TypeScript](https://news.ycombinator.com/ite
14
14
  - [Sandbox Mode](https://amaiya.github.io/patchpal/usage/sandbox/) for secure container isolation
15
15
  - [Python SDK](https://amaiya.github.io/patchpal/usage/python-api/) for flexibility and extensibility
16
16
  - [Built-In](https://amaiya.github.io/patchpal/features/tools/) and [Custom Tools](https://amaiya.github.io/patchpal/features/custom-tools/)
17
+ - [Browser Automation](https://amaiya.github.io/patchpal/features/tools/#browser-automation-14-tools-optional) for interactive web tasks, form filling, and JavaScript-heavy sites
17
18
  - [Skills System](https://amaiya.github.io/patchpal/features/skills/) and [MCP Integration](https://amaiya.github.io/patchpal/features/mcp/)
18
19
  - [Autopilot Mode](https://amaiya.github.io/patchpal/usage/autopilot/) using [Ralph Wiggum loops](https://github.com/amaiya/patchpal/tree/main/examples/ralph/)
19
20
  - [Project Memory](https://amaiya.github.io/patchpal/features/memory/) automatically loads project context from `MEMORY.md` (repository root or `~/.patchpal/repos/<repo-name>/MEMORY.md`) at startup.
@@ -60,6 +61,7 @@ docker run -it --rm \
60
61
 
61
62
  ## Setup
62
63
  0. **Install**: `pip install patchpal`
64
+ - **Optional**: For browser automation (13 additional tools): `pip install patchpal[browser] && python -m playwright install chromium`
63
65
  1. **Get an API key or a Local LLM Engine**:
64
66
  - **[Cloud]** For Anthropic models (default): Sign up at https://console.anthropic.com/
65
67
  - **[Cloud]** For OpenAI models: Get a key from https://platform.openai.com/
@@ -1,6 +1,6 @@
1
1
  """PatchPal - An open-source Claude Code clone implemented purely in Python."""
2
2
 
3
- __version__ = "0.23.2"
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
@@ -1496,6 +1496,92 @@ Repository root takes priority if both exist."""
1496
1496
  "\033[2m❓ Asking user a question...\033[0m",
1497
1497
  flush=True,
1498
1498
  )
1499
+ elif tool_name == "browser_navigate":
1500
+ print(
1501
+ f"\033[2m🌐 Browser navigating: {tool_args.get('url', '')}\033[0m",
1502
+ flush=True,
1503
+ )
1504
+ elif tool_name == "browser_click":
1505
+ print(
1506
+ f"\033[2m🖱️ Browser clicking: {tool_args.get('selector', '')}\033[0m",
1507
+ flush=True,
1508
+ )
1509
+ elif tool_name == "browser_fill":
1510
+ print(
1511
+ f"\033[2m⌨️ Browser filling: {tool_args.get('selector', '')}\033[0m",
1512
+ flush=True,
1513
+ )
1514
+ elif tool_name == "browser_screenshot":
1515
+ print(
1516
+ f"\033[2m📸 Browser screenshot: {tool_args.get('path', '') or 'default path'}\033[0m",
1517
+ flush=True,
1518
+ )
1519
+ elif tool_name == "browser_get_text":
1520
+ print(
1521
+ "\033[2m📄 Browser extracting page text...\033[0m",
1522
+ flush=True,
1523
+ )
1524
+ elif tool_name == "browser_get_html":
1525
+ print(
1526
+ "\033[2m📋 Browser extracting HTML source...\033[0m",
1527
+ flush=True,
1528
+ )
1529
+ elif tool_name == "browser_scroll":
1530
+ direction = tool_args.get("direction", "down")
1531
+ selector = tool_args.get("selector", "")
1532
+ if selector:
1533
+ scroll_desc = f"to '{selector}'"
1534
+ else:
1535
+ scroll_desc = direction
1536
+ print(
1537
+ f"\033[2m📜 Browser scrolling {scroll_desc}...\033[0m",
1538
+ flush=True,
1539
+ )
1540
+ elif tool_name == "browser_execute_script":
1541
+ script_preview = tool_args.get("script", "")[:50]
1542
+ if len(tool_args.get("script", "")) > 50:
1543
+ script_preview += "..."
1544
+ print(
1545
+ f"\033[2m⚡ Browser executing script: {script_preview}\033[0m",
1546
+ flush=True,
1547
+ )
1548
+ elif tool_name == "browser_wait":
1549
+ selector = tool_args.get("selector", "")
1550
+ if selector:
1551
+ wait_desc = f"for '{selector}'"
1552
+ else:
1553
+ wait_desc = f"{tool_args.get('milliseconds', 1000)}ms"
1554
+ print(
1555
+ f"\033[2m⏳ Browser waiting {wait_desc}...\033[0m",
1556
+ flush=True,
1557
+ )
1558
+ elif tool_name == "browser_close":
1559
+ print(
1560
+ "\033[2m🚪 Closing browser...\033[0m",
1561
+ flush=True,
1562
+ )
1563
+ elif tool_name == "browser_list_frames":
1564
+ print(
1565
+ "\033[2m🖼️ Browser listing frames...\033[0m",
1566
+ flush=True,
1567
+ )
1568
+ elif tool_name == "browser_switch_frame":
1569
+ frame_info = ""
1570
+ if "index" in tool_args and tool_args["index"] is not None:
1571
+ frame_info = f"index {tool_args['index']}"
1572
+ elif "name" in tool_args and tool_args["name"] is not None:
1573
+ frame_info = f"'{tool_args['name']}'"
1574
+ else:
1575
+ frame_info = "main page"
1576
+ print(
1577
+ f"\033[2m🖼️ Browser switching to frame: {frame_info}\033[0m",
1578
+ flush=True,
1579
+ )
1580
+ elif tool_name == "browser_dismiss_modals":
1581
+ print(
1582
+ "\033[2m🚫 Browser dismissing modals...\033[0m",
1583
+ flush=True,
1584
+ )
1499
1585
  else:
1500
1586
  # Check if this is an MCP tool by looking for __mcp_server__ attribute
1501
1587
  if hasattr(tool_func, "__mcp_server__"):
@@ -575,6 +575,32 @@ Supported models: Any LiteLLM-supported model
575
575
  pass # Don't fail if audit logging fails
576
576
 
577
577
  print("\nGoodbye!")
578
+
579
+ # Clean up the browser if it was opened during the session.
580
+ # An open Playwright browser keeps its own asyncio machinery
581
+ # alive; closing it here releases the browser process/loop so
582
+ # they don't linger into interpreter shutdown.
583
+ try:
584
+ from patchpal.tools.browser_tools import _BrowserState
585
+
586
+ if _BrowserState.is_open():
587
+ _BrowserState.close(silent=True)
588
+ except Exception:
589
+ pass # Browser tools optional / already closed
590
+
591
+ # Suppress the benign "Task exception was never retrieved"
592
+ # warning that asyncio emits at shutdown: prompt_toolkit's
593
+ # pending Application.run_async() task gets cancelled with a
594
+ # KeyboardInterrupt during teardown, and nobody retrieves it.
595
+ # This is cosmetic and happens AFTER all work is done.
596
+ try:
597
+ import asyncio
598
+
599
+ _loop = asyncio.get_event_loop()
600
+ _loop.set_exception_handler(lambda loop, context: None)
601
+ except Exception:
602
+ pass
603
+
578
604
  break
579
605
 
580
606
  # Handle /help command - show available commands
@@ -1633,7 +1659,25 @@ Supported models: Any LiteLLM-supported model
1633
1659
  )
1634
1660
  continue
1635
1661
  except Exception as e:
1636
- print(f"\n\033[1;31mError:\033[0m {e}")
1662
+ # Debug: Print full exception info to help diagnose asyncio errors
1663
+ import traceback
1664
+
1665
+ error_str = str(e)
1666
+ print(f"\n\033[1;31mError:\033[0m {error_str}")
1667
+
1668
+ # If it's the asyncio error, print more debug info
1669
+ if "asyncio.run() cannot be called from a running event loop" in error_str:
1670
+ print("\n\033[1;33m[DEBUG] AsyncIO Error Details:\033[0m")
1671
+ print(
1672
+ "This error occurs when asyncio.run() is called while an event loop is already running."
1673
+ )
1674
+ print("Stack trace:")
1675
+ traceback.print_exc()
1676
+ print(
1677
+ "\n\033[1;33m[DEBUG] If browser tools were just used, this may be a Playwright cleanup issue.\033[0m"
1678
+ )
1679
+ print("Try: Close any open browser windows and type 'exit' to restart PatchPal.\n")
1680
+
1637
1681
  print("Please try again or type 'exit' to quit.")
1638
1682
 
1639
1683
 
@@ -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}")
@@ -79,6 +79,16 @@ class Config:
79
79
  """Enable web search and fetch tools (default: true)."""
80
80
  return _get_env_bool("PATCHPAL_ENABLE_WEB", "true")
81
81
 
82
+ @property
83
+ def ENABLE_BROWSER(self) -> bool:
84
+ """Enable browser automation tools (default: true).
85
+
86
+ Browser tools require web access, so they are only available when
87
+ ENABLE_WEB is also true (and Playwright is installed). Set this to
88
+ false to disable the browser tools while keeping web_search/web_fetch.
89
+ """
90
+ return _get_env_bool("PATCHPAL_ENABLE_BROWSER", "true")
91
+
82
92
  @property
83
93
  def ENABLE_MCP(self) -> bool:
84
94
  """Enable Model Context Protocol tools (default: true)."""
@@ -358,6 +358,23 @@ class PermissionManager:
358
358
  if self._check_grant_list(self.persistent_grants, tool_name, pattern, full_command):
359
359
  return True
360
360
 
361
+ # Special case: "browser" pattern applies across all browser tools
362
+ # If browser pattern was granted to ANY browser tool, apply to all
363
+ if pattern == "browser":
364
+ browser_tools = [
365
+ "browser_navigate",
366
+ "browser_click",
367
+ "browser_fill",
368
+ "browser_execute_script",
369
+ ]
370
+ for browser_tool in browser_tools:
371
+ if self._check_grant_list(self.session_grants, browser_tool, pattern, full_command):
372
+ return True
373
+ if self._check_grant_list(
374
+ self.persistent_grants, browser_tool, pattern, full_command
375
+ ):
376
+ return True
377
+
361
378
  return False
362
379
 
363
380
  def _grant_permission(
@@ -67,6 +67,42 @@ from patchpal.tools.web_tools import (
67
67
  web_search,
68
68
  )
69
69
 
70
+ # Browser tools (optional - only available if Playwright installed)
71
+ try:
72
+ from patchpal.tools.browser_tools import (
73
+ PLAYWRIGHT_AVAILABLE,
74
+ browser_click,
75
+ browser_close,
76
+ browser_dismiss_modals,
77
+ browser_execute_script,
78
+ browser_fill,
79
+ browser_get_html,
80
+ browser_get_text,
81
+ browser_list_frames,
82
+ browser_navigate,
83
+ browser_press_key,
84
+ browser_screenshot,
85
+ browser_scroll,
86
+ browser_switch_frame,
87
+ browser_wait,
88
+ )
89
+ except ImportError:
90
+ PLAYWRIGHT_AVAILABLE = False
91
+ browser_navigate = None
92
+ browser_click = None
93
+ browser_fill = None
94
+ browser_screenshot = None
95
+ browser_get_text = None
96
+ browser_get_html = None
97
+ browser_scroll = None
98
+ browser_execute_script = None
99
+ browser_press_key = None
100
+ browser_wait = None
101
+ browser_close = None
102
+ browser_dismiss_modals = None
103
+ browser_list_frames = None
104
+ browser_switch_frame = None
105
+
70
106
  __all__ = [
71
107
  # File operations
72
108
  "read_file",
@@ -96,6 +132,22 @@ __all__ = [
96
132
  "web_fetch",
97
133
  "web_search",
98
134
  "get_url_tracker",
135
+ # Browser tools (optional)
136
+ "PLAYWRIGHT_AVAILABLE",
137
+ "browser_navigate",
138
+ "browser_click",
139
+ "browser_fill",
140
+ "browser_screenshot",
141
+ "browser_get_text",
142
+ "browser_get_html",
143
+ "browser_scroll",
144
+ "browser_execute_script",
145
+ "browser_press_key",
146
+ "browser_wait",
147
+ "browser_close",
148
+ "browser_dismiss_modals",
149
+ "browser_list_frames",
150
+ "browser_switch_frame",
99
151
  # Shell tools
100
152
  "run_shell",
101
153
  # User interaction