omnimancer-cli 0.2.10__tar.gz → 0.2.15__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.10 → omnimancer_cli-0.2.15}/PKG-INFO +1 -1
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/agent_loop.py +216 -1
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/command_dispatch.py +5 -2
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/completion.py +13 -1
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/headless.py +45 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/system_prompts.py +182 -5
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/file_system_manager.py +24 -3
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/program_executor.py +9 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent_managers.py +24 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/engine.py +312 -31
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/provider_capabilities.py +52 -13
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/security/permission_controller.py +16 -1
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/security/security_manager.py +11 -0
- omnimancer_cli-0.2.15/omnimancer/providers/__init__.py +43 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/digitalocean.py +1 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/openai.py +48 -14
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/perplexity.py +33 -8
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer_cli.egg-info/PKG-INFO +1 -1
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer_cli.egg-info/SOURCES.txt +7 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/pyproject.toml +1 -1
- omnimancer_cli-0.2.15/tests/test_bedrock_provider.py +787 -0
- omnimancer_cli-0.2.15/tests/test_claude_code_provider.py +624 -0
- omnimancer_cli-0.2.15/tests/test_digitalocean_provider.py +190 -0
- omnimancer_cli-0.2.15/tests/test_mcp_management_commands.py +655 -0
- omnimancer_cli-0.2.15/tests/test_openrouter_provider.py +763 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_perplexity_provider.py +9 -6
- omnimancer_cli-0.2.15/tests/test_provider_capability_consistency.py +195 -0
- omnimancer_cli-0.2.15/tests/test_provider_discovery.py +93 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_rate_limit_fallback.py +1 -4
- omnimancer_cli-0.2.15/tests/test_system_prompts.py +537 -0
- omnimancer_cli-0.2.15/tests/test_vertex_provider.py +777 -0
- omnimancer_cli-0.2.15/tests/test_web_request_markers.py +951 -0
- omnimancer_cli-0.2.10/omnimancer/providers/__init__.py +0 -24
- omnimancer_cli-0.2.10/tests/test_digitalocean_provider.py +0 -92
- omnimancer_cli-0.2.10/tests/test_provider_capability_consistency.py +0 -95
- omnimancer_cli-0.2.10/tests/test_system_prompts.py +0 -43
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/LICENSE +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/README.md +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/__init__.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/__main__.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/__init__.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/approval_formatter.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/approval_integration.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/approval_prompt.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/commands.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/display.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/dynamic_commands.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/handlers/__init__.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/interface.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/subagent.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/cli/tool_handler.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/__init__.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/__init__.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/approval_context.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/approval_interface.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/approval_manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/config.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/read_before_write_errors.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/status_core.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/status_manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/tool_definitions.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/types.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/web_client.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent_engine.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent_mode_manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/chat_manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/config_manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/config_migration.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/config_validator.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/conversation_manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/env_loader.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/fallback_manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/health_monitor.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/history_manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/hooks.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/models.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/provider_initializer.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/provider_registry.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/security/__init__.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/security/approval_workflow.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/security/audit_logger.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/security/permission_rules.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/security/sandbox_manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/signal_handler.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/mcp/__init__.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/mcp/client.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/mcp/manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/azure.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/base.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/bedrock.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/claude.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/claude_code.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/claude_credentials.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/cohere.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/factory.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/gemini.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/mistral.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/ollama.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/openrouter.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/vertex.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/providers/xai.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/ui/__init__.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/ui/cancellation_handler.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/ui/progress_indicator.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/ui/streaming_display.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/utils/__init__.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/utils/error_handler.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/utils/errors.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/utils/retry.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer_cli.egg-info/entry_points.txt +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer_cli.egg-info/requires.txt +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer_cli.egg-info/top_level.txt +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/setup.cfg +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_approval_integration_compatibility.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_approval_preview_format.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_approval_remember.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_approval_system.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_azure_provider.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_claude_credentials.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_claude_provider.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_cohere_provider.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_command_approval_security.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_config_comprehensive.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_conversation_history.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_conversation_manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_core_engine.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_enhanced_model_info.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_env_loader.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_error_handling_robustness.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_execution_timeout_errors.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_fallback_manager.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_fixtures.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_gemini_provider.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_hooks.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_httpx_compatibility.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_import_validation.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_interface_smoke_tests.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_mcp_integration.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_mistral_provider.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_models.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_ollama_provider.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_openai_context_fit.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_openai_provider_tools.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_permission_rules.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_program_executor.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_provider_base.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_provider_integration.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_provider_registry.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_retry.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_security_validation.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_system_integration.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_tool_calling_integration.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_tool_handler.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_workflows_comprehensive.py +0 -0
- {omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/tests/test_xai_provider.py +0 -0
|
@@ -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
|
|
@@ -1259,7 +1259,10 @@ class CommandDispatchMixin:
|
|
|
1259
1259
|
|
|
1260
1260
|
def _show_config_help(self) -> None:
|
|
1261
1261
|
"""Show configuration command help."""
|
|
1262
|
-
|
|
1262
|
+
from ..providers.factory import ProviderFactory
|
|
1263
|
+
|
|
1264
|
+
endpoints = ", ".join(sorted(ProviderFactory.get_available_providers()))
|
|
1265
|
+
help_text = f"""[bold]Configuration Commands:[/bold]
|
|
1263
1266
|
|
|
1264
1267
|
[cyan]/config[/cyan] - Show current configuration
|
|
1265
1268
|
[cyan]/config show[/cyan] - Show current configuration
|
|
@@ -1273,7 +1276,7 @@ class CommandDispatchMixin:
|
|
|
1273
1276
|
[cyan]/config remove-provider <name>[/cyan] - Remove a provider
|
|
1274
1277
|
|
|
1275
1278
|
[bold]Endpoints[/bold] (each is a provider you can configure):
|
|
1276
|
-
|
|
1279
|
+
{endpoints}
|
|
1277
1280
|
|
|
1278
1281
|
[bold]Examples:[/bold]
|
|
1279
1282
|
/config set-provider digitalocean --api-key $DO_KEY \\
|
|
@@ -26,7 +26,19 @@ class CompletionManager:
|
|
|
26
26
|
command = command[1:]
|
|
27
27
|
|
|
28
28
|
static_completions = {
|
|
29
|
-
"mcp": {
|
|
29
|
+
"mcp": {
|
|
30
|
+
0: [
|
|
31
|
+
"status",
|
|
32
|
+
"reload",
|
|
33
|
+
"connect",
|
|
34
|
+
"disconnect",
|
|
35
|
+
"health",
|
|
36
|
+
"servers",
|
|
37
|
+
"tools",
|
|
38
|
+
"add",
|
|
39
|
+
"remove",
|
|
40
|
+
]
|
|
41
|
+
},
|
|
30
42
|
"history": {0: ["list", "clear", "export", "import"]},
|
|
31
43
|
"config": {
|
|
32
44
|
0: [
|
|
@@ -234,12 +234,57 @@ class HeadlessRunner:
|
|
|
234
234
|
self._emitter = HeadlessOutputEmitter(output_format, session_id, verbose)
|
|
235
235
|
self._tokens = TokenAccumulator()
|
|
236
236
|
|
|
237
|
+
@staticmethod
|
|
238
|
+
def _enable_auto_approval(agent_engine: Any) -> None:
|
|
239
|
+
"""Approve every operation, honoring --dangerously-skip-permissions.
|
|
240
|
+
|
|
241
|
+
Headless runs are unattended: no approval callback is ever installed,
|
|
242
|
+
and both approval managers deny by default without one, so a headless
|
|
243
|
+
agent could never write a file or run a command regardless of the
|
|
244
|
+
flag. Config-driven permission DENY rules and hooks still apply — they
|
|
245
|
+
are checked before the approval step.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
async def _approve(_request: Any) -> bool:
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
approval = getattr(agent_engine, "approval", None)
|
|
252
|
+
if approval is not None:
|
|
253
|
+
approval.set_approval_callback(_approve)
|
|
254
|
+
enhanced = getattr(agent_engine, "enhanced_approval", None)
|
|
255
|
+
if enhanced is not None:
|
|
256
|
+
enhanced.set_approval_callback(_approve)
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def _enable_full_trust(agent_engine: Any) -> None:
|
|
260
|
+
"""Relax the hard security layers for unattended full-trust runs.
|
|
261
|
+
|
|
262
|
+
Auto-approval alone is not enough: the command argument sanitizer
|
|
263
|
+
(pipes, &&, redirects), the forbidden-command list, the sensitive
|
|
264
|
+
filename patterns (*key*, *token*, .env, ...), and the 30s default
|
|
265
|
+
command timeout all block operations before/outside the approval
|
|
266
|
+
step, so a --dangerously-skip-permissions run still could not do real
|
|
267
|
+
work. The caller (e.g. a CI or agent container) is the security
|
|
268
|
+
boundary; hard-restricted system paths (~/.ssh, /etc, ...) stay
|
|
269
|
+
blocked.
|
|
270
|
+
"""
|
|
271
|
+
executor = getattr(agent_engine, "executor", None)
|
|
272
|
+
if executor is not None and hasattr(executor, "set_full_trust"):
|
|
273
|
+
executor.set_full_trust(True)
|
|
274
|
+
file_system = getattr(agent_engine, "file_system", None)
|
|
275
|
+
if file_system is not None and hasattr(file_system, "set_full_trust"):
|
|
276
|
+
file_system.set_full_trust(True)
|
|
277
|
+
|
|
237
278
|
async def run(self, prompt: str) -> int:
|
|
238
279
|
agent_engine = getattr(self._engine, "agent_engine", None)
|
|
239
280
|
if not agent_engine:
|
|
240
281
|
self._emitter.emit_error("Agent engine not available.")
|
|
241
282
|
return 1
|
|
242
283
|
|
|
284
|
+
if self._no_approval:
|
|
285
|
+
self._enable_auto_approval(agent_engine)
|
|
286
|
+
self._enable_full_trust(agent_engine)
|
|
287
|
+
|
|
243
288
|
model = self._engine.config_manager.get_config().default_provider or "unknown"
|
|
244
289
|
self._emitter.emit_init(model)
|
|
245
290
|
|
|
@@ -5,7 +5,173 @@ This module contains the system prompts used to guide the AI agent's behavior,
|
|
|
5
5
|
including capability descriptions, operation markers, and execution patterns.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
8
10
|
from pathlib import Path
|
|
11
|
+
from typing import List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Maximum bytes read from any single instruction file. Prevents memory
|
|
16
|
+
# exhaustion if a file is unexpectedly large (accidental or malicious).
|
|
17
|
+
_MAX_INSTRUCTION_FILE_BYTES: int = 100_000 # 100 KB
|
|
18
|
+
|
|
19
|
+
# Regex that matches fenced code blocks (``` … ``` or ~~~ … ~~~).
|
|
20
|
+
# These are stripped before insertion to prevent an attacker from embedding
|
|
21
|
+
# shell commands that the model might treat as executable instructions.
|
|
22
|
+
_FENCED_CODE_BLOCK_RE = re.compile(
|
|
23
|
+
r"^(?:```|~~~)[^\n]*\n.*?^(?:```|~~~)\s*$",
|
|
24
|
+
re.MULTILINE | re.DOTALL,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Regex for control characters that are not ordinary whitespace.
|
|
28
|
+
# Null bytes and C0/C1 control characters can confuse tokenisers or be used
|
|
29
|
+
# to smuggle hidden instructions.
|
|
30
|
+
_CONTROL_CHAR_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _sanitize_instruction_content(raw: str) -> str:
|
|
34
|
+
"""Sanitize raw text loaded from an instruction file.
|
|
35
|
+
|
|
36
|
+
Applies the following transforms in order:
|
|
37
|
+
|
|
38
|
+
1. **Strip fenced code blocks** – removes triple-backtick / triple-tilde
|
|
39
|
+
blocks so embedded shell snippets are not mistaken for executable
|
|
40
|
+
commands by the model.
|
|
41
|
+
|
|
42
|
+
2. **Remove control characters** – null bytes and non-printable C0/C1
|
|
43
|
+
control characters are deleted; ordinary whitespace (\\n, \\r, \\t,
|
|
44
|
+
space) is preserved.
|
|
45
|
+
|
|
46
|
+
3. **Collapse excess blank lines** – more than two consecutive blank lines
|
|
47
|
+
are collapsed to two, keeping the text readable without allowing
|
|
48
|
+
unlimited vertical padding.
|
|
49
|
+
|
|
50
|
+
4. **Truncate to length limit** – content is capped at
|
|
51
|
+
``_MAX_INSTRUCTION_FILE_BYTES`` characters (which is already enforced
|
|
52
|
+
at the read stage, but this acts as a belt-and-suspenders guard).
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
raw: The raw string read from an instruction file.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Sanitized string (may be empty if everything was stripped).
|
|
59
|
+
"""
|
|
60
|
+
# 1. Remove fenced code blocks
|
|
61
|
+
text = _FENCED_CODE_BLOCK_RE.sub("", raw)
|
|
62
|
+
|
|
63
|
+
# 2. Remove dangerous control characters (keep \n \r \t and space)
|
|
64
|
+
text = _CONTROL_CHAR_RE.sub("", text)
|
|
65
|
+
|
|
66
|
+
# 3. Collapse excessive blank lines
|
|
67
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
68
|
+
|
|
69
|
+
# 4. Belt-and-suspenders length cap (file read is already capped)
|
|
70
|
+
text = text[:_MAX_INSTRUCTION_FILE_BYTES]
|
|
71
|
+
|
|
72
|
+
return text.strip()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _read_instruction_file(path: Path) -> Optional[str]:
|
|
76
|
+
"""Read and sanitize an instruction file, enforcing a size cap.
|
|
77
|
+
|
|
78
|
+
Returns sanitized content string, or ``None`` if the file is unreadable,
|
|
79
|
+
empty, or the sanitized result is empty.
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
# Read only up to the byte cap; do not load the entire file first.
|
|
83
|
+
with path.open("r", encoding="utf-8", errors="replace") as fh:
|
|
84
|
+
raw = fh.read(_MAX_INSTRUCTION_FILE_BYTES)
|
|
85
|
+
except OSError as exc:
|
|
86
|
+
logger.debug("Failed to read instruction file %s: %s", path, exc)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
sanitized = _sanitize_instruction_content(raw)
|
|
90
|
+
return sanitized if sanitized else None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_project_instructions() -> str:
|
|
94
|
+
"""Load user-defined instructions from OMNIMANCER.md or CLAUDE.md files.
|
|
95
|
+
|
|
96
|
+
Searches for instruction files and combines them in priority order
|
|
97
|
+
(later entries are more specific and appear last in the prompt):
|
|
98
|
+
|
|
99
|
+
1. Global persona: ``~/.omnimancer/OMNIMANCER.md``
|
|
100
|
+
User-level defaults and persona definitions, always loaded if present.
|
|
101
|
+
|
|
102
|
+
2. Project ``CLAUDE.md``: nearest ancestor of CWD (Claude Code compatibility).
|
|
103
|
+
Useful when a project already ships a CLAUDE.md for Claude Code users.
|
|
104
|
+
|
|
105
|
+
3. Project ``OMNIMANCER.md``: nearest ancestor of CWD (highest priority).
|
|
106
|
+
Omnimancer-specific instructions that override everything else.
|
|
107
|
+
|
|
108
|
+
The upward walk stops at the first ``.git`` directory so instructions
|
|
109
|
+
stay scoped to the project. Files above the git root are ignored as
|
|
110
|
+
project files (only the global config is loaded from ``~/.omnimancer/``).
|
|
111
|
+
|
|
112
|
+
Files that are empty, unreadable, or exceed the size limit are silently
|
|
113
|
+
skipped. Content is sanitised before insertion (see
|
|
114
|
+
:func:`_sanitize_instruction_content`).
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Formatted instruction block string, or empty string if no files found.
|
|
118
|
+
"""
|
|
119
|
+
found: List[Tuple[str, str]] = [] # (label, content)
|
|
120
|
+
|
|
121
|
+
# 1. Global persona / user-level defaults
|
|
122
|
+
global_file = Path.home() / ".omnimancer" / "OMNIMANCER.md"
|
|
123
|
+
if global_file.exists():
|
|
124
|
+
content = _read_instruction_file(global_file)
|
|
125
|
+
if content:
|
|
126
|
+
found.append(("~/.omnimancer/OMNIMANCER.md", content))
|
|
127
|
+
|
|
128
|
+
# 2 & 3. Walk up from CWD to the git root (or filesystem root) looking
|
|
129
|
+
# for project-level CLAUDE.md and OMNIMANCER.md. We collect the *nearest*
|
|
130
|
+
# occurrence of each so subdirectory instructions win over parent ones.
|
|
131
|
+
claude_md_path: Optional[Path] = None
|
|
132
|
+
omnimancer_md_path: Optional[Path] = None
|
|
133
|
+
|
|
134
|
+
check_dir = Path.cwd()
|
|
135
|
+
while True:
|
|
136
|
+
if claude_md_path is None:
|
|
137
|
+
candidate = check_dir / "CLAUDE.md"
|
|
138
|
+
if candidate.exists():
|
|
139
|
+
claude_md_path = candidate
|
|
140
|
+
if omnimancer_md_path is None:
|
|
141
|
+
candidate = check_dir / "OMNIMANCER.md"
|
|
142
|
+
if candidate.exists():
|
|
143
|
+
omnimancer_md_path = candidate
|
|
144
|
+
|
|
145
|
+
# Stop at the git root so we don't wander out of the project
|
|
146
|
+
if (check_dir / ".git").exists():
|
|
147
|
+
break
|
|
148
|
+
parent = check_dir.parent
|
|
149
|
+
if parent == check_dir:
|
|
150
|
+
break # filesystem root
|
|
151
|
+
check_dir = parent
|
|
152
|
+
|
|
153
|
+
if claude_md_path is not None:
|
|
154
|
+
content = _read_instruction_file(claude_md_path)
|
|
155
|
+
if content:
|
|
156
|
+
found.append((claude_md_path.name, content))
|
|
157
|
+
|
|
158
|
+
if omnimancer_md_path is not None:
|
|
159
|
+
content = _read_instruction_file(omnimancer_md_path)
|
|
160
|
+
if content:
|
|
161
|
+
found.append((omnimancer_md_path.name, content))
|
|
162
|
+
|
|
163
|
+
if not found:
|
|
164
|
+
return ""
|
|
165
|
+
|
|
166
|
+
# Wrap with a clear "user-provided" label so the model cannot be tricked
|
|
167
|
+
# into treating this content as authoritative system instructions.
|
|
168
|
+
parts = [
|
|
169
|
+
"\n📋 CUSTOM INSTRUCTIONS (user-provided — not system authority):",
|
|
170
|
+
]
|
|
171
|
+
for label, content in found:
|
|
172
|
+
parts.append(f"\n--- {label} ---\n{content}")
|
|
173
|
+
|
|
174
|
+
return "\n".join(parts)
|
|
9
175
|
|
|
10
176
|
|
|
11
177
|
def get_directory_context() -> str:
|
|
@@ -189,9 +355,8 @@ Examples:
|
|
|
189
355
|
pdf2docx[/COMMAND_EXEC]
|
|
190
356
|
|
|
191
357
|
🌐 Web Operations:
|
|
192
|
-
- [WEB_REQUEST:url] -
|
|
193
|
-
- [WEB_GET:url] - HTTP GET request
|
|
194
|
-
- [WEB_POST:url] data [/WEB_POST] - HTTP POST request
|
|
358
|
+
- [WEB_REQUEST:url] - HTTP GET request to the given URL
|
|
359
|
+
- [WEB_GET:url] - HTTP GET request to the given URL
|
|
195
360
|
|
|
196
361
|
SECURITY & APPROVAL SYSTEM:
|
|
197
362
|
- File writes are AUTOMATICALLY shown to user for approval
|
|
@@ -317,6 +482,7 @@ def build_agent_prompt(supports_tools: bool = False) -> str:
|
|
|
317
482
|
System prompt appropriate for the provider's capabilities
|
|
318
483
|
"""
|
|
319
484
|
directory_info = get_directory_context()
|
|
485
|
+
custom_instructions = load_project_instructions()
|
|
320
486
|
|
|
321
487
|
sections = [
|
|
322
488
|
(
|
|
@@ -338,6 +504,9 @@ def build_agent_prompt(supports_tools: bool = False) -> str:
|
|
|
338
504
|
sections.append(OPERATION_MARKERS_SECTION)
|
|
339
505
|
sections.append(EXECUTION_EXAMPLES_SECTION)
|
|
340
506
|
|
|
507
|
+
if custom_instructions:
|
|
508
|
+
sections.append(custom_instructions)
|
|
509
|
+
|
|
341
510
|
return "\n".join(sections)
|
|
342
511
|
|
|
343
512
|
|
|
@@ -352,6 +521,8 @@ def get_agent_capabilities_prompt() -> str:
|
|
|
352
521
|
Complete system prompt string with directory context
|
|
353
522
|
"""
|
|
354
523
|
directory_info = get_directory_context()
|
|
524
|
+
custom_instructions = load_project_instructions()
|
|
525
|
+
custom_block = f"\n{custom_instructions}" if custom_instructions else ""
|
|
355
526
|
|
|
356
527
|
return f"""SYSTEM: You are an autonomous AI agent with \
|
|
357
528
|
the ability to perform actions on the local system. \
|
|
@@ -364,7 +535,7 @@ You have the following capabilities:
|
|
|
364
535
|
{SECURITY_FEATURES_SECTION}
|
|
365
536
|
{AGENT_EXECUTION_PATTERN_SECTION}
|
|
366
537
|
{OPERATION_MARKERS_SECTION}
|
|
367
|
-
{EXECUTION_EXAMPLES_SECTION}"""
|
|
538
|
+
{EXECUTION_EXAMPLES_SECTION}{custom_block}"""
|
|
368
539
|
|
|
369
540
|
|
|
370
541
|
def get_minimal_prompt() -> str:
|
|
@@ -378,6 +549,8 @@ def get_minimal_prompt() -> str:
|
|
|
378
549
|
Minimal system prompt string
|
|
379
550
|
"""
|
|
380
551
|
directory_info = get_directory_context()
|
|
552
|
+
custom_instructions = load_project_instructions()
|
|
553
|
+
custom_block = f"\n{custom_instructions}" if custom_instructions else ""
|
|
381
554
|
|
|
382
555
|
return f"""SYSTEM: You are an autonomous AI agent with \
|
|
383
556
|
the ability to perform actions on the local system.
|
|
@@ -386,7 +559,7 @@ the ability to perform actions on the local system.
|
|
|
386
559
|
{COMMAND_EXECUTION_SECTION}
|
|
387
560
|
{WEB_OPERATIONS_SECTION}
|
|
388
561
|
{SECURITY_FEATURES_SECTION}
|
|
389
|
-
{OPERATION_MARKERS_SECTION}"""
|
|
562
|
+
{OPERATION_MARKERS_SECTION}{custom_block}"""
|
|
390
563
|
|
|
391
564
|
|
|
392
565
|
def get_custom_prompt(
|
|
@@ -406,6 +579,7 @@ def get_custom_prompt(
|
|
|
406
579
|
Custom system prompt string
|
|
407
580
|
"""
|
|
408
581
|
directory_info = get_directory_context()
|
|
582
|
+
custom_instructions = load_project_instructions()
|
|
409
583
|
|
|
410
584
|
sections = [
|
|
411
585
|
(
|
|
@@ -432,4 +606,7 @@ def get_custom_prompt(
|
|
|
432
606
|
if include_examples:
|
|
433
607
|
sections.append(EXECUTION_EXAMPLES_SECTION)
|
|
434
608
|
|
|
609
|
+
if custom_instructions:
|
|
610
|
+
sections.append(custom_instructions)
|
|
611
|
+
|
|
435
612
|
return "\n".join(sections)
|
{omnimancer_cli-0.2.10 → omnimancer_cli-0.2.15}/omnimancer/core/agent/file_system_manager.py
RENAMED
|
@@ -79,6 +79,14 @@ class FileSystemManager:
|
|
|
79
79
|
# Operation tracking
|
|
80
80
|
self.active_operations: Dict[str, Dict[str, Any]] = {}
|
|
81
81
|
|
|
82
|
+
def set_full_trust(self, enabled: bool) -> None:
|
|
83
|
+
"""Enable/disable full-trust mode on the underlying security manager.
|
|
84
|
+
|
|
85
|
+
In full trust, sensitive name patterns (.env, *key*, *token*, ...)
|
|
86
|
+
stop blocking reads/writes; hard-restricted system paths still deny.
|
|
87
|
+
"""
|
|
88
|
+
self.security.set_full_trust(enabled)
|
|
89
|
+
|
|
82
90
|
async def _request_approval_if_needed(self, operation: Operation) -> bool:
|
|
83
91
|
"""Request approval for operation if approval manager is configured."""
|
|
84
92
|
if not self.require_approval or not self.approval_manager:
|
|
@@ -375,13 +383,22 @@ class FileSystemManager:
|
|
|
375
383
|
path: Union[str, Path],
|
|
376
384
|
encoding: str = "utf-8",
|
|
377
385
|
binary: bool = False,
|
|
386
|
+
allow_sensitive: bool = False,
|
|
378
387
|
) -> Union[str, bytes]:
|
|
379
|
-
"""Read file content with security validation and error handling.
|
|
388
|
+
"""Read file content with security validation and error handling.
|
|
389
|
+
|
|
390
|
+
``allow_sensitive`` permits reading sensitive project-local files (e.g.
|
|
391
|
+
``.env``). It is set for internal reads that are part of an already
|
|
392
|
+
approved operation — such as the read-before-write diff/backup step —
|
|
393
|
+
so the existing content can be shown and preserved.
|
|
394
|
+
"""
|
|
380
395
|
|
|
381
396
|
path = Path(path).resolve()
|
|
382
397
|
|
|
383
398
|
# Security validation
|
|
384
|
-
result = await self.security.secure_file_access(
|
|
399
|
+
result = await self.security.secure_file_access(
|
|
400
|
+
str(path), "read", allow_sensitive=allow_sensitive
|
|
401
|
+
)
|
|
385
402
|
if not result["success"]:
|
|
386
403
|
raise FileOperationError(f"Security check failed: {result['error']}")
|
|
387
404
|
|
|
@@ -1284,7 +1301,11 @@ class FileSystemManager:
|
|
|
1284
1301
|
|
|
1285
1302
|
if file_exists:
|
|
1286
1303
|
try:
|
|
1287
|
-
|
|
1304
|
+
# This read is part of an approved write; allow sensitive
|
|
1305
|
+
# files (e.g. .env) so the diff and backup still work.
|
|
1306
|
+
current_content = await self.read_file(
|
|
1307
|
+
path, encoding=encoding, allow_sensitive=True
|
|
1308
|
+
)
|
|
1288
1309
|
except Exception as e:
|
|
1289
1310
|
# Handle file read error
|
|
1290
1311
|
read_error = FileReadError(str(path), e)
|
|
@@ -86,6 +86,10 @@ class CommandValidator:
|
|
|
86
86
|
"""Validates commands against security policies."""
|
|
87
87
|
|
|
88
88
|
def __init__(self) -> None:
|
|
89
|
+
# Full-trust mode (unattended headless runs where an outer sandbox is
|
|
90
|
+
# the security boundary): argument sanitization is skipped so shell
|
|
91
|
+
# constructs (pipes, &&, redirects) reach the shell executor intact.
|
|
92
|
+
self.full_trust = False
|
|
89
93
|
self.command_categories = {
|
|
90
94
|
CommandCategory.SAFE_READ: {
|
|
91
95
|
"ls",
|
|
@@ -295,6 +299,11 @@ class CommandValidator:
|
|
|
295
299
|
|
|
296
300
|
def validate_command_args(self, command: str, args: List[str]) -> List[str]:
|
|
297
301
|
"""Validate and sanitize command arguments."""
|
|
302
|
+
# Full trust: commands run via the shell on purpose — pipes, &&, and
|
|
303
|
+
# redirects are legitimate agent usage there.
|
|
304
|
+
if self.full_trust:
|
|
305
|
+
return list(args)
|
|
306
|
+
|
|
298
307
|
import re
|
|
299
308
|
|
|
300
309
|
# Define dangerous shell metacharacters and patterns
|