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.
Files changed (70) hide show
  1. {patchpal-0.24.6/patchpal.egg-info → patchpal-0.25.1}/PKG-INFO +1 -1
  2. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/__init__.py +1 -1
  3. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/agent/function_calling.py +28 -5
  4. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/sandbox.py +85 -13
  5. {patchpal-0.24.6 → patchpal-0.25.1/patchpal.egg-info}/PKG-INFO +1 -1
  6. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_enabled_tools.py +86 -0
  7. {patchpal-0.24.6 → patchpal-0.25.1}/LICENSE +0 -0
  8. {patchpal-0.24.6 → patchpal-0.25.1}/MANIFEST.in +0 -0
  9. {patchpal-0.24.6 → patchpal-0.25.1}/README.md +0 -0
  10. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/agent/__init__.py +0 -0
  11. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/agent/bedrock_profile_utils.py +0 -0
  12. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/agent/react.py +0 -0
  13. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/__init__.py +0 -0
  14. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/autopilot.py +0 -0
  15. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/interactive.py +0 -0
  16. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/mcp.py +0 -0
  17. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/cli/streaming.py +0 -0
  18. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/config.py +0 -0
  19. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/context.py +0 -0
  20. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/permissions.py +0 -0
  21. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/prompts/react_prompt.md +0 -0
  22. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/prompts/system_prompt.md +0 -0
  23. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/skills.py +0 -0
  24. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/__init__.py +0 -0
  25. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/audit.py +0 -0
  26. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/browser_tools.py +0 -0
  27. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/code_analysis.py +0 -0
  28. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/common.py +0 -0
  29. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/definitions.py +0 -0
  30. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/file_reading.py +0 -0
  31. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/file_writing.py +0 -0
  32. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/find_tool.py +0 -0
  33. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/grep_tool.py +0 -0
  34. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/image_handler.py +0 -0
  35. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/mcp.py +0 -0
  36. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/repo_map.py +0 -0
  37. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/shell_tools.py +0 -0
  38. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/todo_tools.py +0 -0
  39. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/tool_schema.py +0 -0
  40. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/user_interaction.py +0 -0
  41. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal/tools/web_tools.py +0 -0
  42. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal.egg-info/SOURCES.txt +0 -0
  43. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal.egg-info/dependency_links.txt +0 -0
  44. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal.egg-info/entry_points.txt +0 -0
  45. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal.egg-info/requires.txt +0 -0
  46. {patchpal-0.24.6 → patchpal-0.25.1}/patchpal.egg-info/top_level.txt +0 -0
  47. {patchpal-0.24.6 → patchpal-0.25.1}/pyproject.toml +0 -0
  48. {patchpal-0.24.6 → patchpal-0.25.1}/setup.cfg +0 -0
  49. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_agent.py +0 -0
  50. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_cli.py +0 -0
  51. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_config_dynamic.py +0 -0
  52. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_context.py +0 -0
  53. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_custom_tools.py +0 -0
  54. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_find_tool.py +0 -0
  55. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_guardrails.py +0 -0
  56. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_image_blocking.py +0 -0
  57. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_maximum_security.py +0 -0
  58. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_mcp_config.py +0 -0
  59. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_memory.py +0 -0
  60. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_memory_locations.py +0 -0
  61. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_operational_safety.py +0 -0
  62. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_optional_tools.py +0 -0
  63. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_permissions.py +0 -0
  64. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_react.py +0 -0
  65. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_reasoning_content.py +0 -0
  66. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_repo_map.py +0 -0
  67. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_simplified_prompt.py +0 -0
  68. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_skills.py +0 -0
  69. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_streaming.py +0 -0
  70. {patchpal-0.24.6 → patchpal-0.25.1}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.24.6
3
+ Version: 0.25.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.6"
3
+ __version__ = "0.25.1"
4
4
 
5
5
  from patchpal.agent import create_agent, create_react_agent
6
6
  from patchpal.cli.autopilot import autopilot_loop
@@ -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) or TOOL_FUNCTIONS.get(
1362
- tool_name
1363
- )
1364
- if tool_func is None:
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
- else:
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
- # 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.24.6
3
+ Version: 0.25.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
@@ -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