omnimancer-cli 0.2.7__tar.gz → 0.2.13__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 (160) hide show
  1. {omnimancer_cli-0.2.7/omnimancer_cli.egg-info → omnimancer_cli-0.2.13}/PKG-INFO +5 -2
  2. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/README.md +4 -1
  3. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/agent_loop.py +216 -1
  4. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/approval_integration.py +44 -19
  5. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/command_dispatch.py +302 -31
  6. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/commands.py +5 -1
  7. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/completion.py +13 -1
  8. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/headless.py +87 -22
  9. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/interface.py +159 -32
  10. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/system_prompts.py +182 -5
  11. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/tool_handler.py +66 -3
  12. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/file_system_manager.py +16 -3
  13. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/chat_manager.py +18 -4
  14. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/engine.py +569 -39
  15. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/models.py +114 -1
  16. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/provider_capabilities.py +52 -13
  17. omnimancer_cli-0.2.13/omnimancer/core/rate_limit_fallback.py +245 -0
  18. omnimancer_cli-0.2.13/omnimancer/providers/__init__.py +43 -0
  19. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/base.py +10 -0
  20. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/digitalocean.py +1 -0
  21. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/openai.py +101 -22
  22. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/perplexity.py +33 -8
  23. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13/omnimancer_cli.egg-info}/PKG-INFO +5 -2
  24. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer_cli.egg-info/SOURCES.txt +9 -0
  25. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/pyproject.toml +1 -1
  26. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_approval_preview_format.py +72 -1
  27. omnimancer_cli-0.2.13/tests/test_bedrock_provider.py +787 -0
  28. omnimancer_cli-0.2.13/tests/test_claude_code_provider.py +624 -0
  29. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_core_engine.py +186 -0
  30. omnimancer_cli-0.2.13/tests/test_digitalocean_provider.py +190 -0
  31. omnimancer_cli-0.2.13/tests/test_mcp_management_commands.py +655 -0
  32. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_models.py +65 -0
  33. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_openai_provider_tools.py +141 -0
  34. omnimancer_cli-0.2.13/tests/test_openrouter_provider.py +763 -0
  35. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_perplexity_provider.py +9 -6
  36. omnimancer_cli-0.2.13/tests/test_provider_capability_consistency.py +195 -0
  37. omnimancer_cli-0.2.13/tests/test_provider_discovery.py +93 -0
  38. omnimancer_cli-0.2.13/tests/test_rate_limit_fallback.py +575 -0
  39. omnimancer_cli-0.2.13/tests/test_system_prompts.py +537 -0
  40. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_tool_calling_integration.py +244 -11
  41. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_tool_handler.py +82 -0
  42. omnimancer_cli-0.2.13/tests/test_vertex_provider.py +777 -0
  43. omnimancer_cli-0.2.13/tests/test_web_request_markers.py +951 -0
  44. omnimancer_cli-0.2.7/omnimancer/providers/__init__.py +0 -24
  45. omnimancer_cli-0.2.7/tests/test_digitalocean_provider.py +0 -92
  46. omnimancer_cli-0.2.7/tests/test_provider_capability_consistency.py +0 -95
  47. omnimancer_cli-0.2.7/tests/test_system_prompts.py +0 -43
  48. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/LICENSE +0 -0
  49. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/__init__.py +0 -0
  50. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/__main__.py +0 -0
  51. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/__init__.py +0 -0
  52. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/approval_formatter.py +0 -0
  53. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/approval_prompt.py +0 -0
  54. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/display.py +0 -0
  55. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/dynamic_commands.py +0 -0
  56. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/handlers/__init__.py +0 -0
  57. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/subagent.py +0 -0
  58. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/__init__.py +0 -0
  59. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/__init__.py +0 -0
  60. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/approval_context.py +0 -0
  61. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/approval_interface.py +0 -0
  62. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/approval_manager.py +0 -0
  63. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/config.py +0 -0
  64. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/program_executor.py +0 -0
  65. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/read_before_write_errors.py +0 -0
  66. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/status_core.py +0 -0
  67. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/status_manager.py +0 -0
  68. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/tool_definitions.py +0 -0
  69. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/types.py +0 -0
  70. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/web_client.py +0 -0
  71. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
  72. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent_engine.py +0 -0
  73. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent_managers.py +0 -0
  74. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent_mode_manager.py +0 -0
  75. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/config_manager.py +0 -0
  76. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/config_migration.py +0 -0
  77. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/config_validator.py +0 -0
  78. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/conversation_manager.py +0 -0
  79. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/env_loader.py +0 -0
  80. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/fallback_manager.py +0 -0
  81. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/health_monitor.py +0 -0
  82. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/history_manager.py +0 -0
  83. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/hooks.py +0 -0
  84. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/mcp_integration_layer.py +0 -0
  85. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/provider_initializer.py +0 -0
  86. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/provider_registry.py +0 -0
  87. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/__init__.py +0 -0
  88. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/approval_workflow.py +0 -0
  89. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/audit_logger.py +0 -0
  90. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/permission_controller.py +0 -0
  91. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/permission_rules.py +0 -0
  92. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/sandbox_manager.py +0 -0
  93. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/security_manager.py +0 -0
  94. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/signal_handler.py +0 -0
  95. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/mcp/__init__.py +0 -0
  96. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/mcp/client.py +0 -0
  97. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/mcp/manager.py +0 -0
  98. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/azure.py +0 -0
  99. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/bedrock.py +0 -0
  100. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/claude.py +0 -0
  101. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/claude_code.py +0 -0
  102. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/claude_credentials.py +0 -0
  103. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/cohere.py +0 -0
  104. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/factory.py +0 -0
  105. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/gemini.py +0 -0
  106. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/mistral.py +0 -0
  107. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/ollama.py +0 -0
  108. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/openrouter.py +0 -0
  109. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/vertex.py +0 -0
  110. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/xai.py +0 -0
  111. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/ui/__init__.py +0 -0
  112. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/ui/cancellation_handler.py +0 -0
  113. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/ui/progress_indicator.py +0 -0
  114. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/ui/streaming_display.py +0 -0
  115. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/utils/__init__.py +0 -0
  116. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/utils/error_handler.py +0 -0
  117. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/utils/errors.py +0 -0
  118. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/utils/retry.py +0 -0
  119. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
  120. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer_cli.egg-info/entry_points.txt +0 -0
  121. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer_cli.egg-info/requires.txt +0 -0
  122. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer_cli.egg-info/top_level.txt +0 -0
  123. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/setup.cfg +0 -0
  124. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_approval_integration_compatibility.py +0 -0
  125. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_approval_remember.py +0 -0
  126. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_approval_system.py +0 -0
  127. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_azure_provider.py +0 -0
  128. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_claude_credentials.py +0 -0
  129. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_claude_provider.py +0 -0
  130. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_cohere_provider.py +0 -0
  131. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_command_approval_security.py +0 -0
  132. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_config_comprehensive.py +0 -0
  133. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_conversation_history.py +0 -0
  134. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_conversation_manager.py +0 -0
  135. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_enhanced_model_info.py +0 -0
  136. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_env_loader.py +0 -0
  137. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_error_handling_robustness.py +0 -0
  138. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_execution_timeout_errors.py +0 -0
  139. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_fallback_manager.py +0 -0
  140. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_fixtures.py +0 -0
  141. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_gemini_provider.py +0 -0
  142. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_hooks.py +0 -0
  143. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_httpx_compatibility.py +0 -0
  144. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_import_validation.py +0 -0
  145. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_interface_smoke_tests.py +0 -0
  146. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_mcp_integration.py +0 -0
  147. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_mcp_integration_layer.py +0 -0
  148. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_mistral_provider.py +0 -0
  149. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_ollama_provider.py +0 -0
  150. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_openai_context_fit.py +0 -0
  151. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_permission_rules.py +0 -0
  152. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_program_executor.py +0 -0
  153. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_provider_base.py +0 -0
  154. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_provider_integration.py +0 -0
  155. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_provider_registry.py +0 -0
  156. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_retry.py +0 -0
  157. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_security_validation.py +0 -0
  158. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_system_integration.py +0 -0
  159. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_workflows_comprehensive.py +0 -0
  160. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_xai_provider.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omnimancer-cli
3
- Version: 0.2.7
3
+ Version: 0.2.13
4
4
  Summary: A unified command-line interface for multiple AI language models
5
5
  Author-email: Kellan Strong <kellan@jite.ai>
6
6
  License: MIT
@@ -96,6 +96,9 @@ omn -p "fix the failing tests" --dangerously-skip-permissions
96
96
 
97
97
  # Verbose output / explicit config file
98
98
  omn -p "audit the security module" --verbose --config ~/.omnimancer/config.json
99
+
100
+ # Raise the tool-iteration cap for big jobs (default: 25)
101
+ omn -p "migrate every test file to pytest fixtures" --max-iterations 100
99
102
  ```
100
103
 
101
104
  The `--provider`, `--model`, and `--base-url` flags are **session overrides**: they
@@ -240,7 +243,7 @@ Tool names match the agent's toolset (`Read`, `Write`, `Edit`, `Bash`, `Glob`, `
240
243
  | `/help [command]` | Show help (optionally for a specific command) |
241
244
  | `/quit` | Exit (also: `/exit`, Ctrl+D) |
242
245
  | `/clear` | Clear terminal screen |
243
- | `/switch <provider> [model]` | Switch provider or model |
246
+ | `/switch <provider> [model]` | Switch provider or model (uncataloged models are registered on the fly) |
244
247
  | `/models [filter]` | List available models (alias: `/model`) |
245
248
  | `/providers` | List all providers with status |
246
249
  | `/agent on\|off\|status` | Toggle agent mode |
@@ -45,6 +45,9 @@ omn -p "fix the failing tests" --dangerously-skip-permissions
45
45
 
46
46
  # Verbose output / explicit config file
47
47
  omn -p "audit the security module" --verbose --config ~/.omnimancer/config.json
48
+
49
+ # Raise the tool-iteration cap for big jobs (default: 25)
50
+ omn -p "migrate every test file to pytest fixtures" --max-iterations 100
48
51
  ```
49
52
 
50
53
  The `--provider`, `--model`, and `--base-url` flags are **session overrides**: they
@@ -189,7 +192,7 @@ Tool names match the agent's toolset (`Read`, `Write`, `Edit`, `Bash`, `Glob`, `
189
192
  | `/help [command]` | Show help (optionally for a specific command) |
190
193
  | `/quit` | Exit (also: `/exit`, Ctrl+D) |
191
194
  | `/clear` | Clear terminal screen |
192
- | `/switch <provider> [model]` | Switch provider or model |
195
+ | `/switch <provider> [model]` | Switch provider or model (uncataloged models are registered on the fly) |
193
196
  | `/models [filter]` | List available models (alias: `/model`) |
194
197
  | `/providers` | List all providers with status |
195
198
  | `/agent on\|off\|status` | Toggle agent mode |
@@ -6,6 +6,7 @@ via the agent engine, with a continuous workflow loop for multi-step tasks.
6
6
  """
7
7
 
8
8
  import asyncio
9
+ import ipaddress
9
10
  import logging
10
11
  import re
11
12
  import shlex
@@ -13,9 +14,98 @@ import subprocess
13
14
  from difflib import SequenceMatcher
14
15
  from pathlib import Path
15
16
  from typing import Any, Optional
17
+ from urllib.parse import urlparse, urlunparse
16
18
 
17
19
  from ..core.agent.types import Operation, OperationType
18
20
 
21
+ # ---------------------------------------------------------------------------
22
+ # Web request constants and helpers
23
+ # ---------------------------------------------------------------------------
24
+
25
+ #: Maximum characters returned from a single web response.
26
+ _MAX_WEB_RESPONSE_CHARS: int = 10_000
27
+
28
+ #: Hostnames that must never be contacted – cloud/VM metadata endpoints.
29
+ _BLOCKED_METADATA_HOSTNAMES: frozenset = frozenset(
30
+ {
31
+ "169.254.169.254", # AWS IMDSv1/v2, GCP, Azure shared endpoint
32
+ "metadata.google.internal", # GCP metadata server
33
+ "fd00:ec2::254", # AWS IPv6 IMDS
34
+ "metadata.internal", # Azure internal alias
35
+ }
36
+ )
37
+
38
+ #: Compiled patterns for web operation markers.
39
+ _WEB_GET_PATTERN = re.compile(r"\[WEB_GET:([^\]]+)\]")
40
+ _WEB_REQUEST_PATTERN = re.compile(r"\[WEB_REQUEST:([^\]]+)\]")
41
+ _WEB_POST_PATTERN = re.compile(r"\[WEB_POST:([^\]]+)\]")
42
+
43
+
44
+ def _check_url_safety(url: str) -> Optional[str]:
45
+ """Validate whether *url* is safe to request.
46
+
47
+ Enforces:
48
+ - HTTP or HTTPS schemes only.
49
+ - No embedded credentials (``user:pass@host``).
50
+ - No loopback, private, link-local, or reserved addresses.
51
+ - No known cloud/VM metadata endpoints.
52
+
53
+ Returns an error-message string when the URL is unsafe, ``None`` if OK.
54
+ """
55
+ try:
56
+ parsed = urlparse(url.strip())
57
+ except Exception:
58
+ return "Invalid URL format"
59
+
60
+ if parsed.scheme not in ("http", "https"):
61
+ return f"Unsupported scheme '{parsed.scheme}':" " only http/https allowed"
62
+
63
+ if parsed.username or parsed.password:
64
+ return "URLs with embedded credentials are not allowed"
65
+
66
+ hostname = parsed.hostname
67
+ if not hostname:
68
+ return "URL has no hostname"
69
+
70
+ hostname_lower = hostname.lower()
71
+
72
+ # Block well-known localhost names before IP parsing.
73
+ if hostname_lower in ("localhost", "::1", "0.0.0.0"):
74
+ return f"Requests to '{hostname}' are not allowed"
75
+
76
+ # Block cloud metadata hostnames.
77
+ if hostname_lower in _BLOCKED_METADATA_HOSTNAMES:
78
+ return f"Requests to cloud metadata endpoint '{hostname}' are blocked"
79
+
80
+ # Reject private/reserved IP addresses.
81
+ try:
82
+ addr = ipaddress.ip_address(hostname_lower)
83
+ if (
84
+ addr.is_private
85
+ or addr.is_loopback
86
+ or addr.is_link_local
87
+ or addr.is_reserved
88
+ ):
89
+ return f"Requests to private/reserved IP '{hostname}'" " are not allowed"
90
+ except ValueError:
91
+ pass # Hostname is a domain name – that's fine.
92
+
93
+ return None # URL passed all safety checks.
94
+
95
+
96
+ def _sanitize_url_for_log(url: str) -> str:
97
+ """Return *url* with credentials and query-string stripped for safe logging."""
98
+ try:
99
+ parsed = urlparse(url)
100
+ netloc = parsed.hostname or ""
101
+ if parsed.port:
102
+ netloc = f"{netloc}:{parsed.port}"
103
+ safe = parsed._replace(netloc=netloc, query="", fragment="")
104
+ return urlunparse(safe)
105
+ except Exception:
106
+ return "<invalid-url>"
107
+
108
+
19
109
  logger = logging.getLogger(__name__)
20
110
 
21
111
 
@@ -32,6 +122,113 @@ class AgentLoopMixin:
32
122
  def _show_assistant_message(self, content: str, model: str) -> None: ...
33
123
  def _show_error(self, message: str) -> None: ...
34
124
 
125
+ async def _execute_web_request(
126
+ self,
127
+ marker_text: str,
128
+ url: str,
129
+ http_method: str,
130
+ current_response: str,
131
+ ) -> tuple[str, bool]:
132
+ """Execute a single web-request marker and return the updated response.
133
+
134
+ Validates the *url* for safety, requests user approval, issues the
135
+ request via the agent engine, and inlines the result.
136
+
137
+ Args:
138
+ marker_text: The raw marker string to replace (e.g. ``[WEB_GET:…]``).
139
+ url: The target URL extracted from the marker.
140
+ http_method: HTTP verb – ``"GET"`` or ``"POST"``.
141
+ current_response: The response text being built up.
142
+
143
+ Returns:
144
+ A ``(updated_response, was_cancelled)`` tuple. When ``was_cancelled``
145
+ is ``True`` the returned string already contains the
146
+ ``__WORKFLOW_CANCELLED__`` sentinel and the caller must return
147
+ immediately.
148
+ """
149
+ safe_url = _sanitize_url_for_log(url)
150
+
151
+ url_error = _check_url_safety(url)
152
+ if url_error:
153
+ return (
154
+ current_response.replace(
155
+ marker_text,
156
+ f"❌ Web request blocked: {url_error}",
157
+ ),
158
+ False,
159
+ )
160
+
161
+ agent_engine = getattr(self.engine, "agent_engine", None)
162
+ if not agent_engine:
163
+ return (
164
+ current_response.replace(
165
+ marker_text,
166
+ "❌ Agent engine not available"
167
+ " - web requests disabled for security",
168
+ ),
169
+ False,
170
+ )
171
+
172
+ operation = Operation(
173
+ type=OperationType.WEB_REQUEST,
174
+ description=f"{http_method} request to: {safe_url}",
175
+ data={"url": url, "method": http_method},
176
+ requires_approval=True,
177
+ )
178
+
179
+ try:
180
+ result = await agent_engine.execute_with_approval(operation)
181
+
182
+ if result.success:
183
+ if isinstance(result.data, dict):
184
+ content = result.data.get("content", "")
185
+ if not isinstance(content, str):
186
+ content = str(content)
187
+ else:
188
+ content = str(result.data or "")
189
+
190
+ if len(content) > _MAX_WEB_RESPONSE_CHARS:
191
+ content = (
192
+ content[:_MAX_WEB_RESPONSE_CHARS]
193
+ + "\n\n[... response truncated ...]"
194
+ )
195
+
196
+ return (
197
+ current_response.replace(
198
+ marker_text,
199
+ f"🌐 {http_method} {safe_url}\n{content}",
200
+ ),
201
+ False,
202
+ )
203
+ else:
204
+ if result.was_cancelled:
205
+ updated = current_response.replace(
206
+ marker_text,
207
+ "🚫 Agent workflow cancelled by user",
208
+ )
209
+ return updated + "\n\n__WORKFLOW_CANCELLED__", True
210
+ else:
211
+ return (
212
+ current_response.replace(
213
+ marker_text,
214
+ f"❌ Web request failed: {result.error}",
215
+ ),
216
+ False,
217
+ )
218
+
219
+ except Exception as e:
220
+ logger.error(
221
+ f"Web request execution failed: {e}",
222
+ exc_info=True,
223
+ )
224
+ return (
225
+ current_response.replace(
226
+ marker_text,
227
+ f"❌ Web request error: {str(e)}",
228
+ ),
229
+ False,
230
+ )
231
+
35
232
  async def _execute_continuous_workflow(
36
233
  self, original_message: str, initial_response: Any
37
234
  ) -> None:
@@ -68,7 +265,9 @@ class AgentLoopMixin:
68
265
  r"\[FILE_WRITE:[^\]]+\\?\].*?\[/FILE_WRITE\]",
69
266
  r"\[FILE_READ:[^\]]+\\?\]",
70
267
  r"\[COMMAND_EXEC\].*?\[/COMMAND_EXEC\]",
71
- r"\[WEB_REQUEST:[^\]]+\\?\]",
268
+ r"\[WEB_REQUEST:[^\]]+\]",
269
+ r"\[WEB_GET:[^\]]+\]",
270
+ r"\[WEB_POST:[^\]]+\]",
72
271
  r"\[SAFE_EXEC\].*?\[/SAFE_EXEC\]",
73
272
  ]
74
273
 
@@ -622,6 +821,22 @@ class AgentLoopMixin:
622
821
  f"❌ Command execution error: {str(e)}",
623
822
  )
624
823
 
824
+ # WEB_GET, WEB_REQUEST, and WEB_POST markers – each handled via the
825
+ # shared _execute_web_request helper which validates safety, requests
826
+ # user approval, and inlines the response content.
827
+ for marker_pattern, http_method in (
828
+ (_WEB_GET_PATTERN, "GET"),
829
+ (_WEB_REQUEST_PATTERN, "GET"),
830
+ (_WEB_POST_PATTERN, "POST"),
831
+ ):
832
+ for match in marker_pattern.finditer(response_content):
833
+ url = match.group(1).strip()
834
+ updated_response, cancelled = await self._execute_web_request(
835
+ match.group(0), url, http_method, updated_response
836
+ )
837
+ if cancelled:
838
+ return updated_response
839
+
625
840
  except Exception as e:
626
841
  self._show_error(f"Error parsing operations: {e}")
627
842
  return response_content
@@ -13,7 +13,11 @@ from typing import Any, Dict, List, Optional
13
13
 
14
14
  from rich.console import Console
15
15
 
16
- from ..core.agent.approval_manager import BatchApprovalRequest, EnhancedApprovalManager
16
+ from ..core.agent.approval_manager import (
17
+ BatchApprovalRequest,
18
+ ChangePreview,
19
+ EnhancedApprovalManager,
20
+ )
17
21
  from ..core.agent.types import Operation, OperationType
18
22
  from ..core.security.approval_workflow import ApprovalRequest, RiskLevel
19
23
  from ..core.security.permission_controller import PermissionController
@@ -181,24 +185,7 @@ class CLIApprovalIntegration:
181
185
  risk_level=RiskLevel.MEDIUM, # Default, can be enhanced
182
186
  )
183
187
 
184
- # Create change preview from operation
185
- # Map OperationType to ChangeType (they have matching values)
186
- from ..core.agent.approval_manager import ChangePreview, ChangeType
187
-
188
- try:
189
- change_type = ChangeType(operation.type.value)
190
- except ValueError:
191
- # Default to FILE_MODIFY if type not in ChangeType enum
192
- change_type = ChangeType.FILE_MODIFY
193
-
194
- change_preview = ChangePreview(
195
- change_type=change_type,
196
- description=operation.description,
197
- # Use string preview as proposed_state
198
- proposed_state=operation.preview,
199
- metadata=operation.data,
200
- reversible=operation.reversible,
201
- )
188
+ change_preview = await self._build_change_preview(operation)
202
189
 
203
190
  # Present approval dialog and get user decision
204
191
  decision = await self.prompt_handler.prompt_for_approval(
@@ -226,6 +213,44 @@ class CLIApprovalIntegration:
226
213
  logger.error(f"Error in single approval handler: {e}", exc_info=True)
227
214
  return (False, False)
228
215
 
216
+ async def _build_change_preview(self, operation: Operation) -> ChangePreview:
217
+ """Build the ChangePreview shown in the approval dialog.
218
+
219
+ File writes and deletes go through the approval manager's rich
220
+ generators, which read the current file and produce a real unified
221
+ diff. Other operation types get a generic preview built from the
222
+ operation's own description/preview strings.
223
+ """
224
+ from ..core.agent.approval_manager import ChangePreview, ChangeType
225
+
226
+ if operation.type in (OperationType.FILE_WRITE, OperationType.FILE_DELETE):
227
+ try:
228
+ preview = await self.approval_manager.generate_operation_preview(
229
+ operation
230
+ )
231
+ # The operation's description ("Edit file: ...") is more
232
+ # specific than the generator's generic one.
233
+ if operation.description:
234
+ preview.description = operation.description
235
+ return preview
236
+ except Exception as e:
237
+ logger.debug(f"Rich preview failed, using generic preview: {e}")
238
+
239
+ try:
240
+ change_type = ChangeType(operation.type.value)
241
+ except ValueError:
242
+ # Default to FILE_MODIFY if type not in ChangeType enum
243
+ change_type = ChangeType.FILE_MODIFY
244
+
245
+ return ChangePreview(
246
+ change_type=change_type,
247
+ description=operation.description,
248
+ # Use string preview as proposed_state
249
+ proposed_state=operation.preview,
250
+ metadata=operation.data,
251
+ reversible=operation.reversible,
252
+ )
253
+
229
254
  async def _handle_batch_approval(
230
255
  self, batch_request: BatchApprovalRequest
231
256
  ) -> Dict[str, Any]: