omnimancer-cli 0.2.26__tar.gz → 0.2.27__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.26/omnimancer_cli.egg-info → omnimancer_cli-0.2.27}/PKG-INFO +1 -1
- omnimancer_cli-0.2.27/omnimancer/core/security/sandbox_manager.py +555 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27/omnimancer_cli.egg-info}/PKG-INFO +1 -1
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/pyproject.toml +1 -1
- omnimancer_cli-0.2.26/omnimancer/core/security/sandbox_manager.py +0 -381
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/LICENSE +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/README.md +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omn_fleet_hook.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/__main__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/agent_loop.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/approval_formatter.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/approval_integration.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/approval_prompt.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/command_dispatch.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/commands.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/completion.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/display.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/dynamic_commands.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/file_mentions.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/handlers/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/headless.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/interface.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/prompt_input.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/pt_completion.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/session.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/subagent.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/system_prompts.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/tool_handler.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/turn_notify.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/usage.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/approval_context.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/approval_interface.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/approval_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/config.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/file_system_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/program_executor.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/read_before_write_errors.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/status_core.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/status_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/tool_definitions.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/types.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/web_client.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent_engine.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent_managers.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent_mode_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/chat_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/config_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/config_migration.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/config_validator.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/conversation_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/engine.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/env_loader.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/fallback_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/health_monitor.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/history_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/hooks.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/models.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/prompt_enhancer.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/provider_capabilities.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/provider_initializer.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/provider_registry.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/approval_workflow.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/audit_logger.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/permission_controller.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/permission_rules.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/security_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/signal_handler.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/events/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/events/emitter.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/events/jsonl_writer.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/events/schema.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/mcp/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/mcp/client.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/mcp/manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/azure.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/base.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/bedrock.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/claude.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/claude_code.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/claude_credentials.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/cohere.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/digitalocean.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/factory.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/gemini.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/mistral.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/ollama.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/openai.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/openai_compatible.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/openrouter.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/perplexity.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/vertex.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/xai.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/app.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/cli.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/models.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/sources.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/widgets.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/ui/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/ui/cancellation_handler.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/ui/progress_indicator.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/ui/streaming_display.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/ui/turn_activity.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/utils/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/utils/error_handler.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/utils/errors.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/utils/retry.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer_cli.egg-info/SOURCES.txt +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer_cli.egg-info/entry_points.txt +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer_cli.egg-info/requires.txt +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer_cli.egg-info/top_level.txt +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/setup.cfg +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_approval_integration_compatibility.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_approval_preview_format.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_approval_remember.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_approval_system.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_azure_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_bedrock_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_claude_code_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_claude_credentials.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_claude_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_cohere_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_command_approval_security.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_config_comprehensive.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_config_migration_passthrough.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_conversation_history.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_conversation_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_core_engine.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_digitalocean_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_enhanced_model_info.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_env_loader.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_error_handling_robustness.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_execution_timeout_errors.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_fallback_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_fixtures.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_gemini_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_hooks.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_httpx_compatibility.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_import_validation.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_interface_smoke_tests.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_mcp_integration.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_mcp_management_commands.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_mistral_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_models.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_ollama_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openai_compatible_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openai_context_fit.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openai_provider_tools.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openai_request_timeout.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openai_streaming.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openrouter_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_permission_rules.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_perplexity_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_program_executor.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_aliases.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_base.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_capability_consistency.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_discovery.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_integration.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_registry.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_retry.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_security_validation.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_system_integration.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_system_prompts.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_tool_calling_integration.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_tool_handler.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_vertex_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_web_request_markers.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_workflows_comprehensive.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_xai_provider.py +0 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"""Sandbox manager for isolating and limiting agent operations."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import resource
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
import psutil
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# How long to wait for a terminated process to flush its output pipes.
|
|
19
|
+
OUTPUT_DRAIN_TIMEOUT_SECONDS = 5
|
|
20
|
+
|
|
21
|
+
# How often the monitor thread samples a sandboxed process.
|
|
22
|
+
MONITOR_POLL_INTERVAL_SECONDS = 1.0
|
|
23
|
+
|
|
24
|
+
# How long to wait for the monitor thread to notice a finished process.
|
|
25
|
+
MONITOR_JOIN_TIMEOUT_SECONDS = 5
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _timeout_reason(timeout_seconds: int) -> str:
|
|
29
|
+
"""Build the termination reason used when a command exceeds its timeout."""
|
|
30
|
+
return f"Command timed out after {timeout_seconds} seconds"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _limit_reason(limit_name: str, actual: str, allowed: str) -> str:
|
|
34
|
+
"""Build the termination reason used when a resource limit is exceeded."""
|
|
35
|
+
return f"Command exceeded {limit_name} limit ({actual} > {allowed})"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ResourceLimits:
|
|
39
|
+
"""Resource limits for sandboxed operations."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
max_memory_mb: int = 512,
|
|
44
|
+
max_cpu_seconds: int = 30,
|
|
45
|
+
max_file_size_mb: int = 100,
|
|
46
|
+
max_open_files: int = 100,
|
|
47
|
+
max_processes: int = 10,
|
|
48
|
+
timeout_seconds: int = 60,
|
|
49
|
+
):
|
|
50
|
+
self.max_memory_mb = max_memory_mb
|
|
51
|
+
self.max_cpu_seconds = max_cpu_seconds
|
|
52
|
+
self.max_file_size_mb = max_file_size_mb
|
|
53
|
+
self.max_open_files = max_open_files
|
|
54
|
+
self.max_processes = max_processes
|
|
55
|
+
self.timeout_seconds = timeout_seconds
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SandboxedProcess:
|
|
59
|
+
"""Represents a process running in a sandbox."""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self, process: subprocess.Popen, temp_dir: str, limits: ResourceLimits
|
|
63
|
+
):
|
|
64
|
+
self.process = process
|
|
65
|
+
self.temp_dir = temp_dir
|
|
66
|
+
self.limits = limits
|
|
67
|
+
self.start_time = time.time()
|
|
68
|
+
self.monitor_thread: Optional[threading.Thread] = None
|
|
69
|
+
self.terminated = False
|
|
70
|
+
# Why the process was forcibly stopped, if it was. Used to explain a
|
|
71
|
+
# non-zero exit code that would otherwise carry no diagnostic output.
|
|
72
|
+
self.termination_reason: Optional[str] = None
|
|
73
|
+
# Signals the monitor thread to stop sampling immediately instead of
|
|
74
|
+
# sitting out the rest of its poll interval.
|
|
75
|
+
self.stop_event = threading.Event()
|
|
76
|
+
self._lock = threading.Lock()
|
|
77
|
+
|
|
78
|
+
def is_running(self) -> bool:
|
|
79
|
+
"""Check if process is still running."""
|
|
80
|
+
return self.process.poll() is None and not self.terminated
|
|
81
|
+
|
|
82
|
+
def terminate(self, reason: Optional[str] = None) -> None:
|
|
83
|
+
"""Terminate the sandboxed process, recording why it was stopped.
|
|
84
|
+
|
|
85
|
+
Safe to call from several threads: the first caller wins and its
|
|
86
|
+
``reason`` is the one reported back to the caller of
|
|
87
|
+
:meth:`SandboxManager.execute_sandboxed_command`.
|
|
88
|
+
"""
|
|
89
|
+
with self._lock:
|
|
90
|
+
if self.terminated:
|
|
91
|
+
return
|
|
92
|
+
self.terminated = True
|
|
93
|
+
if reason is not None and self.termination_reason is None:
|
|
94
|
+
self.termination_reason = reason
|
|
95
|
+
|
|
96
|
+
self.stop_event.set()
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
if self.process.poll() is None:
|
|
100
|
+
self.process.terminate()
|
|
101
|
+
# Give it a moment to terminate gracefully
|
|
102
|
+
try:
|
|
103
|
+
self.process.wait(timeout=OUTPUT_DRAIN_TIMEOUT_SECONDS)
|
|
104
|
+
except subprocess.TimeoutExpired:
|
|
105
|
+
self.process.kill()
|
|
106
|
+
except (ProcessLookupError, OSError) as e:
|
|
107
|
+
logger.debug(
|
|
108
|
+
"Could not terminate sandboxed process %s: %s", self.process.pid, e
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def cleanup(self) -> None:
|
|
112
|
+
"""Clean up temporary resources."""
|
|
113
|
+
self.terminate()
|
|
114
|
+
if os.path.exists(self.temp_dir):
|
|
115
|
+
try:
|
|
116
|
+
shutil.rmtree(self.temp_dir)
|
|
117
|
+
except (OSError, PermissionError) as e:
|
|
118
|
+
logger.warning("Could not remove sandbox directory: %s", e)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class SandboxManager:
|
|
122
|
+
"""Manages sandboxed execution of agent operations."""
|
|
123
|
+
|
|
124
|
+
def __init__(self, default_limits: Optional[ResourceLimits] = None):
|
|
125
|
+
self.default_limits = default_limits or ResourceLimits()
|
|
126
|
+
self.active_processes: Dict[int, SandboxedProcess] = {}
|
|
127
|
+
self.temp_base_dir = tempfile.gettempdir()
|
|
128
|
+
|
|
129
|
+
def create_sandbox_environment(
|
|
130
|
+
self, limits: Optional[ResourceLimits] = None
|
|
131
|
+
) -> str:
|
|
132
|
+
"""Create a temporary sandbox directory."""
|
|
133
|
+
limits = limits or self.default_limits
|
|
134
|
+
|
|
135
|
+
# Create temporary directory for sandbox
|
|
136
|
+
temp_dir = tempfile.mkdtemp(prefix="omnimancer_sandbox_")
|
|
137
|
+
|
|
138
|
+
# Set up basic directory structure
|
|
139
|
+
os.makedirs(os.path.join(temp_dir, "workspace"), exist_ok=True)
|
|
140
|
+
os.makedirs(os.path.join(temp_dir, "output"), exist_ok=True)
|
|
141
|
+
|
|
142
|
+
# Create sandbox info file
|
|
143
|
+
info_file = os.path.join(temp_dir, "sandbox_info.txt")
|
|
144
|
+
with open(info_file, "w") as f:
|
|
145
|
+
f.write(f"Sandbox created at: {time.ctime()}\n")
|
|
146
|
+
f.write(f"Memory limit: {limits.max_memory_mb} MB\n")
|
|
147
|
+
f.write(f"CPU limit: {limits.max_cpu_seconds} seconds\n")
|
|
148
|
+
f.write(f"Timeout: {limits.timeout_seconds} seconds\n")
|
|
149
|
+
|
|
150
|
+
return temp_dir
|
|
151
|
+
|
|
152
|
+
def execute_sandboxed_command(
|
|
153
|
+
self,
|
|
154
|
+
command: List[str],
|
|
155
|
+
working_dir: Optional[str] = None,
|
|
156
|
+
env_vars: Optional[Dict[str, str]] = None,
|
|
157
|
+
limits: Optional[ResourceLimits] = None,
|
|
158
|
+
input_data: Optional[str] = None,
|
|
159
|
+
) -> Dict[str, Any]:
|
|
160
|
+
"""Execute a command in a sandboxed environment."""
|
|
161
|
+
|
|
162
|
+
limits = limits or self.default_limits
|
|
163
|
+
sandbox_dir = self.create_sandbox_environment(limits)
|
|
164
|
+
process: Optional[subprocess.Popen] = None
|
|
165
|
+
sandboxed_proc: Optional[SandboxedProcess] = None
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
# Prepare environment
|
|
169
|
+
sandbox_env = os.environ.copy()
|
|
170
|
+
if env_vars:
|
|
171
|
+
sandbox_env.update(env_vars)
|
|
172
|
+
|
|
173
|
+
# Restrict environment variables
|
|
174
|
+
sandbox_env = self._filter_environment_variables(sandbox_env)
|
|
175
|
+
|
|
176
|
+
# Set working directory
|
|
177
|
+
work_dir = working_dir or os.path.join(sandbox_dir, "workspace")
|
|
178
|
+
|
|
179
|
+
# Surface unenforceable limits before forking (see the helper).
|
|
180
|
+
self._warn_about_unenforceable_limits(limits)
|
|
181
|
+
|
|
182
|
+
# Prepare process arguments
|
|
183
|
+
process_args = {
|
|
184
|
+
"args": command,
|
|
185
|
+
"cwd": work_dir,
|
|
186
|
+
"env": sandbox_env,
|
|
187
|
+
"stdin": subprocess.PIPE if input_data else None,
|
|
188
|
+
"stdout": subprocess.PIPE,
|
|
189
|
+
"stderr": subprocess.PIPE,
|
|
190
|
+
"text": True,
|
|
191
|
+
"preexec_fn": self._setup_process_limits(limits),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Start process
|
|
195
|
+
process = subprocess.Popen(**process_args) # type: ignore[call-overload]
|
|
196
|
+
sandboxed_proc = SandboxedProcess(process, sandbox_dir, limits)
|
|
197
|
+
|
|
198
|
+
# Track the process
|
|
199
|
+
self.active_processes[process.pid] = sandboxed_proc
|
|
200
|
+
|
|
201
|
+
# Start monitoring
|
|
202
|
+
sandboxed_proc.monitor_thread = threading.Thread(
|
|
203
|
+
target=self._monitor_process,
|
|
204
|
+
args=(sandboxed_proc,),
|
|
205
|
+
daemon=True,
|
|
206
|
+
)
|
|
207
|
+
sandboxed_proc.monitor_thread.start()
|
|
208
|
+
|
|
209
|
+
# Execute and wait for completion
|
|
210
|
+
try:
|
|
211
|
+
stdout, stderr = process.communicate(
|
|
212
|
+
input=input_data, timeout=limits.timeout_seconds
|
|
213
|
+
)
|
|
214
|
+
return_code = process.returncode
|
|
215
|
+
|
|
216
|
+
except subprocess.TimeoutExpired:
|
|
217
|
+
sandboxed_proc.terminate(_timeout_reason(limits.timeout_seconds))
|
|
218
|
+
stdout, stderr = self._drain_output(process)
|
|
219
|
+
# The deadline was missed, so this is a failure even if the
|
|
220
|
+
# process happened to exit cleanly while being terminated.
|
|
221
|
+
signalled_code = process.returncode
|
|
222
|
+
return self._build_result(
|
|
223
|
+
return_code=(
|
|
224
|
+
signalled_code if signalled_code not in (None, 0) else -1
|
|
225
|
+
),
|
|
226
|
+
stdout=stdout,
|
|
227
|
+
stderr=stderr,
|
|
228
|
+
sandbox_dir=sandbox_dir,
|
|
229
|
+
termination_reason=sandboxed_proc.termination_reason,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# The monitor thread may have killed the process before it could
|
|
233
|
+
# write any diagnostics (for example when a limit was exceeded just
|
|
234
|
+
# under the communicate() timeout). Surface that reason instead of
|
|
235
|
+
# returning a bare non-zero exit code with empty output.
|
|
236
|
+
return self._build_result(
|
|
237
|
+
return_code=return_code,
|
|
238
|
+
stdout=stdout,
|
|
239
|
+
stderr=stderr,
|
|
240
|
+
sandbox_dir=sandbox_dir,
|
|
241
|
+
termination_reason=sandboxed_proc.termination_reason,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.exception("Sandboxed command execution failed")
|
|
246
|
+
return {
|
|
247
|
+
"success": False,
|
|
248
|
+
"return_code": -1,
|
|
249
|
+
"stdout": "",
|
|
250
|
+
"stderr": f"Sandbox execution error: {str(e)}",
|
|
251
|
+
"sandbox_dir": sandbox_dir,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
finally:
|
|
255
|
+
self._release_process(process, sandboxed_proc)
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _build_result(
|
|
259
|
+
return_code: Optional[int],
|
|
260
|
+
stdout: Optional[str],
|
|
261
|
+
stderr: Optional[str],
|
|
262
|
+
sandbox_dir: str,
|
|
263
|
+
termination_reason: Optional[str] = None,
|
|
264
|
+
) -> Dict[str, Any]:
|
|
265
|
+
"""Assemble the result mapping returned to callers.
|
|
266
|
+
|
|
267
|
+
A recorded ``termination_reason`` only overrides the outcome when the
|
|
268
|
+
command did not already exit cleanly; a process that finished
|
|
269
|
+
successfully in the same instant it was signalled is still a success.
|
|
270
|
+
"""
|
|
271
|
+
code = -1 if return_code is None else return_code
|
|
272
|
+
success = code == 0
|
|
273
|
+
stderr_text = stderr or ""
|
|
274
|
+
|
|
275
|
+
if termination_reason and not success:
|
|
276
|
+
stderr_text = (
|
|
277
|
+
f"{stderr_text.rstrip()}\n{termination_reason}".lstrip()
|
|
278
|
+
if stderr_text.strip()
|
|
279
|
+
else termination_reason
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"success": success,
|
|
284
|
+
"return_code": code,
|
|
285
|
+
"stdout": stdout or "",
|
|
286
|
+
"stderr": stderr_text,
|
|
287
|
+
"sandbox_dir": sandbox_dir,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def _drain_output(process: subprocess.Popen) -> Tuple[str, str]:
|
|
292
|
+
"""Collect whatever a stopped process managed to write to its pipes."""
|
|
293
|
+
for attempt in range(2):
|
|
294
|
+
try:
|
|
295
|
+
stdout, stderr = process.communicate(
|
|
296
|
+
timeout=OUTPUT_DRAIN_TIMEOUT_SECONDS
|
|
297
|
+
)
|
|
298
|
+
return stdout or "", stderr or ""
|
|
299
|
+
except subprocess.TimeoutExpired:
|
|
300
|
+
# A child of the sandboxed process may still hold the pipes
|
|
301
|
+
# open; kill the group leader once and retry, then give up.
|
|
302
|
+
if attempt == 0:
|
|
303
|
+
process.kill()
|
|
304
|
+
continue
|
|
305
|
+
logger.warning(
|
|
306
|
+
"Timed out draining output of sandboxed process %s", process.pid
|
|
307
|
+
)
|
|
308
|
+
except (OSError, ValueError) as e:
|
|
309
|
+
logger.warning(
|
|
310
|
+
"Could not drain output of sandboxed process %s: %s",
|
|
311
|
+
process.pid,
|
|
312
|
+
e,
|
|
313
|
+
)
|
|
314
|
+
break
|
|
315
|
+
return "", ""
|
|
316
|
+
|
|
317
|
+
def _release_process(
|
|
318
|
+
self,
|
|
319
|
+
process: Optional[subprocess.Popen],
|
|
320
|
+
sandboxed_proc: Optional[SandboxedProcess],
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Stop monitoring, clean up the sandbox and untrack the process."""
|
|
323
|
+
if sandboxed_proc is None:
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
# cleanup() terminates the process, which wakes the monitor thread.
|
|
327
|
+
sandboxed_proc.cleanup()
|
|
328
|
+
|
|
329
|
+
if sandboxed_proc.monitor_thread is not None:
|
|
330
|
+
sandboxed_proc.monitor_thread.join(timeout=MONITOR_JOIN_TIMEOUT_SECONDS)
|
|
331
|
+
if sandboxed_proc.monitor_thread.is_alive():
|
|
332
|
+
logger.warning("Monitor thread did not stop within the join timeout")
|
|
333
|
+
|
|
334
|
+
if process is not None:
|
|
335
|
+
self.active_processes.pop(process.pid, None)
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def _resource_limit_values(limits: ResourceLimits) -> Dict[int, Tuple[int, int]]:
|
|
339
|
+
"""Map each ``RLIMIT_*`` constant to the soft/hard value to apply."""
|
|
340
|
+
return {
|
|
341
|
+
resource.RLIMIT_AS: (limits.max_memory_mb * 1024 * 1024,) * 2,
|
|
342
|
+
resource.RLIMIT_CPU: (limits.max_cpu_seconds,) * 2,
|
|
343
|
+
resource.RLIMIT_FSIZE: (limits.max_file_size_mb * 1024 * 1024,) * 2,
|
|
344
|
+
resource.RLIMIT_NOFILE: (limits.max_open_files,) * 2,
|
|
345
|
+
resource.RLIMIT_NPROC: (limits.max_processes,) * 2,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
def _warn_about_unenforceable_limits(self, limits: ResourceLimits) -> None:
|
|
349
|
+
"""Warn, before forking, about limits the kernel will refuse to apply.
|
|
350
|
+
|
|
351
|
+
``setrlimit`` runs post-fork where logging is not safe, so any
|
|
352
|
+
diagnostics have to be produced here in the parent. The monitor thread
|
|
353
|
+
still enforces memory, CPU and runtime caps if a limit cannot be set.
|
|
354
|
+
"""
|
|
355
|
+
for limit_id, (requested, _) in self._resource_limit_values(limits).items():
|
|
356
|
+
try:
|
|
357
|
+
_, hard = resource.getrlimit(limit_id)
|
|
358
|
+
except (OSError, ValueError) as e:
|
|
359
|
+
logger.warning("Could not read resource limit %s: %s", limit_id, e)
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
if hard != resource.RLIM_INFINITY and requested > hard:
|
|
363
|
+
logger.warning(
|
|
364
|
+
"Requested resource limit %s (%s) exceeds the hard limit (%s); "
|
|
365
|
+
"falling back to monitor-based enforcement",
|
|
366
|
+
limit_id,
|
|
367
|
+
requested,
|
|
368
|
+
hard,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def _setup_process_limits(self, limits: ResourceLimits) -> Callable:
|
|
372
|
+
"""Create a function to set up process resource limits."""
|
|
373
|
+
|
|
374
|
+
limit_values = self._resource_limit_values(limits)
|
|
375
|
+
|
|
376
|
+
def setup_limits() -> None:
|
|
377
|
+
# Runs in the forked child before exec: only async-signal-safe work
|
|
378
|
+
# is allowed here, so failures cannot be logged or reported. They
|
|
379
|
+
# are non-fatal by design -- a limit the kernel refuses (common in
|
|
380
|
+
# containers) is reported by _warn_about_unenforceable_limits() and
|
|
381
|
+
# still enforced by the monitor thread.
|
|
382
|
+
for limit_id, values in limit_values.items():
|
|
383
|
+
try:
|
|
384
|
+
resource.setrlimit(limit_id, values)
|
|
385
|
+
except (OSError, ValueError):
|
|
386
|
+
continue
|
|
387
|
+
|
|
388
|
+
return setup_limits
|
|
389
|
+
|
|
390
|
+
def _monitor_process(self, sandboxed_proc: SandboxedProcess) -> None:
|
|
391
|
+
"""Monitor a sandboxed process for resource violations."""
|
|
392
|
+
|
|
393
|
+
limits = sandboxed_proc.limits
|
|
394
|
+
pid = sandboxed_proc.process.pid
|
|
395
|
+
|
|
396
|
+
while sandboxed_proc.is_running():
|
|
397
|
+
try:
|
|
398
|
+
# Check if process still exists
|
|
399
|
+
if sandboxed_proc.process.poll() is not None:
|
|
400
|
+
break
|
|
401
|
+
|
|
402
|
+
# Get process info
|
|
403
|
+
try:
|
|
404
|
+
proc_info = psutil.Process(pid)
|
|
405
|
+
|
|
406
|
+
# Check memory usage
|
|
407
|
+
memory_mb = proc_info.memory_info().rss / (1024 * 1024)
|
|
408
|
+
if memory_mb > limits.max_memory_mb:
|
|
409
|
+
self._terminate_for_limit(
|
|
410
|
+
sandboxed_proc,
|
|
411
|
+
_limit_reason(
|
|
412
|
+
"memory",
|
|
413
|
+
f"{memory_mb:.1f} MB",
|
|
414
|
+
f"{limits.max_memory_mb} MB",
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
# Check CPU time
|
|
420
|
+
cpu_times = proc_info.cpu_times()
|
|
421
|
+
cpu_time = cpu_times.user + cpu_times.system
|
|
422
|
+
if cpu_time > limits.max_cpu_seconds:
|
|
423
|
+
self._terminate_for_limit(
|
|
424
|
+
sandboxed_proc,
|
|
425
|
+
_limit_reason(
|
|
426
|
+
"CPU time",
|
|
427
|
+
f"{cpu_time:.1f}s",
|
|
428
|
+
f"{limits.max_cpu_seconds}s",
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
break
|
|
432
|
+
|
|
433
|
+
# Check total runtime
|
|
434
|
+
runtime = time.time() - sandboxed_proc.start_time
|
|
435
|
+
if runtime > limits.timeout_seconds:
|
|
436
|
+
self._terminate_for_limit(
|
|
437
|
+
sandboxed_proc, _timeout_reason(limits.timeout_seconds)
|
|
438
|
+
)
|
|
439
|
+
break
|
|
440
|
+
|
|
441
|
+
except psutil.NoSuchProcess:
|
|
442
|
+
# Process already terminated
|
|
443
|
+
break
|
|
444
|
+
|
|
445
|
+
# Wait before the next check, waking early if the process is
|
|
446
|
+
# terminated in the meantime.
|
|
447
|
+
if sandboxed_proc.stop_event.wait(MONITOR_POLL_INTERVAL_SECONDS):
|
|
448
|
+
break
|
|
449
|
+
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.warning("Error monitoring sandboxed process %s: %s", pid, e)
|
|
452
|
+
break
|
|
453
|
+
|
|
454
|
+
@staticmethod
|
|
455
|
+
def _terminate_for_limit(sandboxed_proc: SandboxedProcess, reason: str) -> None:
|
|
456
|
+
"""Stop a process that violated a resource limit and record why."""
|
|
457
|
+
logger.warning(
|
|
458
|
+
"Terminating sandboxed process %s: %s", sandboxed_proc.process.pid, reason
|
|
459
|
+
)
|
|
460
|
+
sandboxed_proc.terminate(reason)
|
|
461
|
+
|
|
462
|
+
def _filter_environment_variables(self, env: Dict[str, str]) -> Dict[str, str]:
|
|
463
|
+
"""Filter environment variables to remove sensitive ones."""
|
|
464
|
+
|
|
465
|
+
# List of sensitive environment variable patterns
|
|
466
|
+
sensitive_patterns = [
|
|
467
|
+
"PASSWORD",
|
|
468
|
+
"SECRET",
|
|
469
|
+
"TOKEN",
|
|
470
|
+
"KEY",
|
|
471
|
+
"CREDENTIAL",
|
|
472
|
+
"AWS_",
|
|
473
|
+
"AZURE_",
|
|
474
|
+
"GCP_",
|
|
475
|
+
"GOOGLE_",
|
|
476
|
+
"SSH_",
|
|
477
|
+
"HOME",
|
|
478
|
+
"USER",
|
|
479
|
+
"USERNAME",
|
|
480
|
+
]
|
|
481
|
+
|
|
482
|
+
filtered_env = {}
|
|
483
|
+
for key, value in env.items():
|
|
484
|
+
# Keep only safe environment variables
|
|
485
|
+
if not any(pattern in key.upper() for pattern in sensitive_patterns):
|
|
486
|
+
filtered_env[key] = value
|
|
487
|
+
|
|
488
|
+
# Add minimal required variables
|
|
489
|
+
filtered_env.update(
|
|
490
|
+
{
|
|
491
|
+
"PATH": "/usr/local/bin:/usr/bin:/bin",
|
|
492
|
+
"LANG": "en_US.UTF-8",
|
|
493
|
+
"LC_ALL": "en_US.UTF-8",
|
|
494
|
+
}
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
return filtered_env
|
|
498
|
+
|
|
499
|
+
@contextmanager # type: ignore[arg-type]
|
|
500
|
+
def sandbox_context( # type: ignore[misc]
|
|
501
|
+
self,
|
|
502
|
+
limits: Optional[ResourceLimits] = None,
|
|
503
|
+
) -> None:
|
|
504
|
+
"""Context manager for sandbox operations."""
|
|
505
|
+
|
|
506
|
+
sandbox_dir = self.create_sandbox_environment(limits)
|
|
507
|
+
try:
|
|
508
|
+
yield sandbox_dir
|
|
509
|
+
finally:
|
|
510
|
+
if os.path.exists(sandbox_dir):
|
|
511
|
+
try:
|
|
512
|
+
shutil.rmtree(sandbox_dir)
|
|
513
|
+
except (OSError, PermissionError):
|
|
514
|
+
pass
|
|
515
|
+
|
|
516
|
+
def cleanup_all_sandboxes(self) -> None:
|
|
517
|
+
"""Clean up all active sandboxes."""
|
|
518
|
+
|
|
519
|
+
for proc in list(self.active_processes.values()):
|
|
520
|
+
proc.cleanup()
|
|
521
|
+
self.active_processes.clear()
|
|
522
|
+
|
|
523
|
+
def get_active_process_count(self) -> int:
|
|
524
|
+
"""Get number of active sandboxed processes."""
|
|
525
|
+
return len(self.active_processes)
|
|
526
|
+
|
|
527
|
+
def get_sandbox_info(self, process_id: int) -> Optional[Dict[str, Any]]:
|
|
528
|
+
"""Get information about a sandboxed process."""
|
|
529
|
+
|
|
530
|
+
if process_id not in self.active_processes:
|
|
531
|
+
return None
|
|
532
|
+
|
|
533
|
+
proc = self.active_processes[process_id]
|
|
534
|
+
try:
|
|
535
|
+
proc_info = psutil.Process(process_id)
|
|
536
|
+
return {
|
|
537
|
+
"pid": process_id,
|
|
538
|
+
"is_running": proc.is_running(),
|
|
539
|
+
"start_time": proc.start_time,
|
|
540
|
+
"runtime": time.time() - proc.start_time,
|
|
541
|
+
"memory_mb": proc_info.memory_info().rss / (1024 * 1024),
|
|
542
|
+
"cpu_percent": proc_info.cpu_percent(),
|
|
543
|
+
"sandbox_dir": proc.temp_dir,
|
|
544
|
+
"limits": {
|
|
545
|
+
"max_memory_mb": proc.limits.max_memory_mb,
|
|
546
|
+
"max_cpu_seconds": proc.limits.max_cpu_seconds,
|
|
547
|
+
"timeout_seconds": proc.limits.timeout_seconds,
|
|
548
|
+
},
|
|
549
|
+
}
|
|
550
|
+
except psutil.NoSuchProcess:
|
|
551
|
+
return {
|
|
552
|
+
"pid": process_id,
|
|
553
|
+
"is_running": False,
|
|
554
|
+
"sandbox_dir": proc.temp_dir,
|
|
555
|
+
}
|