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.
- {omnimancer_cli-0.2.7/omnimancer_cli.egg-info → omnimancer_cli-0.2.13}/PKG-INFO +5 -2
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/README.md +4 -1
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/agent_loop.py +216 -1
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/approval_integration.py +44 -19
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/command_dispatch.py +302 -31
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/commands.py +5 -1
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/completion.py +13 -1
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/headless.py +87 -22
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/interface.py +159 -32
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/system_prompts.py +182 -5
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/tool_handler.py +66 -3
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/file_system_manager.py +16 -3
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/chat_manager.py +18 -4
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/engine.py +569 -39
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/models.py +114 -1
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/provider_capabilities.py +52 -13
- omnimancer_cli-0.2.13/omnimancer/core/rate_limit_fallback.py +245 -0
- omnimancer_cli-0.2.13/omnimancer/providers/__init__.py +43 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/base.py +10 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/digitalocean.py +1 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/openai.py +101 -22
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/perplexity.py +33 -8
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13/omnimancer_cli.egg-info}/PKG-INFO +5 -2
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer_cli.egg-info/SOURCES.txt +9 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/pyproject.toml +1 -1
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_approval_preview_format.py +72 -1
- omnimancer_cli-0.2.13/tests/test_bedrock_provider.py +787 -0
- omnimancer_cli-0.2.13/tests/test_claude_code_provider.py +624 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_core_engine.py +186 -0
- omnimancer_cli-0.2.13/tests/test_digitalocean_provider.py +190 -0
- omnimancer_cli-0.2.13/tests/test_mcp_management_commands.py +655 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_models.py +65 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_openai_provider_tools.py +141 -0
- omnimancer_cli-0.2.13/tests/test_openrouter_provider.py +763 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_perplexity_provider.py +9 -6
- omnimancer_cli-0.2.13/tests/test_provider_capability_consistency.py +195 -0
- omnimancer_cli-0.2.13/tests/test_provider_discovery.py +93 -0
- omnimancer_cli-0.2.13/tests/test_rate_limit_fallback.py +575 -0
- omnimancer_cli-0.2.13/tests/test_system_prompts.py +537 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_tool_calling_integration.py +244 -11
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_tool_handler.py +82 -0
- omnimancer_cli-0.2.13/tests/test_vertex_provider.py +777 -0
- omnimancer_cli-0.2.13/tests/test_web_request_markers.py +951 -0
- omnimancer_cli-0.2.7/omnimancer/providers/__init__.py +0 -24
- omnimancer_cli-0.2.7/tests/test_digitalocean_provider.py +0 -92
- omnimancer_cli-0.2.7/tests/test_provider_capability_consistency.py +0 -95
- omnimancer_cli-0.2.7/tests/test_system_prompts.py +0 -43
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/LICENSE +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/__init__.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/__main__.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/__init__.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/approval_formatter.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/approval_prompt.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/display.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/dynamic_commands.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/handlers/__init__.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/cli/subagent.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/__init__.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/__init__.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/approval_context.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/approval_interface.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/approval_manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/config.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/program_executor.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/read_before_write_errors.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/status_core.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/status_manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/tool_definitions.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/types.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/web_client.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent_engine.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent_managers.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/agent_mode_manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/config_manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/config_migration.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/config_validator.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/conversation_manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/env_loader.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/fallback_manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/health_monitor.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/history_manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/hooks.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/provider_initializer.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/provider_registry.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/__init__.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/approval_workflow.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/audit_logger.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/permission_controller.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/permission_rules.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/sandbox_manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/security/security_manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/core/signal_handler.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/mcp/__init__.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/mcp/client.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/mcp/manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/azure.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/bedrock.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/claude.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/claude_code.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/claude_credentials.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/cohere.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/factory.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/gemini.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/mistral.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/ollama.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/openrouter.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/vertex.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/providers/xai.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/ui/__init__.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/ui/cancellation_handler.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/ui/progress_indicator.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/ui/streaming_display.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/utils/__init__.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/utils/error_handler.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/utils/errors.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer/utils/retry.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer_cli.egg-info/entry_points.txt +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer_cli.egg-info/requires.txt +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/omnimancer_cli.egg-info/top_level.txt +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/setup.cfg +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_approval_integration_compatibility.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_approval_remember.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_approval_system.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_azure_provider.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_claude_credentials.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_claude_provider.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_cohere_provider.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_command_approval_security.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_config_comprehensive.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_conversation_history.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_conversation_manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_enhanced_model_info.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_env_loader.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_error_handling_robustness.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_execution_timeout_errors.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_fallback_manager.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_fixtures.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_gemini_provider.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_hooks.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_httpx_compatibility.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_import_validation.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_interface_smoke_tests.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_mcp_integration.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_mistral_provider.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_ollama_provider.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_openai_context_fit.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_permission_rules.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_program_executor.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_provider_base.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_provider_integration.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_provider_registry.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_retry.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_security_validation.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_system_integration.py +0 -0
- {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.13}/tests/test_workflows_comprehensive.py +0 -0
- {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.
|
|
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
|
|
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
|
-
|
|
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]:
|