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.
Files changed (183) hide show
  1. {omnimancer_cli-0.2.26/omnimancer_cli.egg-info → omnimancer_cli-0.2.27}/PKG-INFO +1 -1
  2. omnimancer_cli-0.2.27/omnimancer/core/security/sandbox_manager.py +555 -0
  3. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27/omnimancer_cli.egg-info}/PKG-INFO +1 -1
  4. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/pyproject.toml +1 -1
  5. omnimancer_cli-0.2.26/omnimancer/core/security/sandbox_manager.py +0 -381
  6. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/LICENSE +0 -0
  7. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/README.md +0 -0
  8. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omn_fleet_hook.py +0 -0
  9. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/__init__.py +0 -0
  10. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/__main__.py +0 -0
  11. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/__init__.py +0 -0
  12. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/agent_loop.py +0 -0
  13. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/approval_formatter.py +0 -0
  14. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/approval_integration.py +0 -0
  15. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/approval_prompt.py +0 -0
  16. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/command_dispatch.py +0 -0
  17. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/commands.py +0 -0
  18. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/completion.py +0 -0
  19. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/display.py +0 -0
  20. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/dynamic_commands.py +0 -0
  21. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/file_mentions.py +0 -0
  22. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/handlers/__init__.py +0 -0
  23. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/headless.py +0 -0
  24. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/interface.py +0 -0
  25. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/prompt_input.py +0 -0
  26. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/pt_completion.py +0 -0
  27. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/session.py +0 -0
  28. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/subagent.py +0 -0
  29. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/system_prompts.py +0 -0
  30. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/tool_handler.py +0 -0
  31. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/turn_notify.py +0 -0
  32. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/cli/usage.py +0 -0
  33. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/__init__.py +0 -0
  34. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/__init__.py +0 -0
  35. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/approval_context.py +0 -0
  36. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/approval_interface.py +0 -0
  37. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/approval_manager.py +0 -0
  38. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/config.py +0 -0
  39. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/file_system_manager.py +0 -0
  40. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/program_executor.py +0 -0
  41. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/read_before_write_errors.py +0 -0
  42. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/status_core.py +0 -0
  43. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/status_manager.py +0 -0
  44. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/tool_definitions.py +0 -0
  45. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/types.py +0 -0
  46. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/web_client.py +0 -0
  47. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
  48. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent_engine.py +0 -0
  49. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent_managers.py +0 -0
  50. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/agent_mode_manager.py +0 -0
  51. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/chat_manager.py +0 -0
  52. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/config_manager.py +0 -0
  53. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/config_migration.py +0 -0
  54. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/config_validator.py +0 -0
  55. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/conversation_manager.py +0 -0
  56. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/engine.py +0 -0
  57. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/env_loader.py +0 -0
  58. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/fallback_manager.py +0 -0
  59. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/health_monitor.py +0 -0
  60. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/history_manager.py +0 -0
  61. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/hooks.py +0 -0
  62. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/mcp_integration_layer.py +0 -0
  63. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/models.py +0 -0
  64. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/prompt_enhancer.py +0 -0
  65. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/provider_capabilities.py +0 -0
  66. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/provider_initializer.py +0 -0
  67. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/provider_registry.py +0 -0
  68. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/rate_limit_fallback.py +0 -0
  69. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/__init__.py +0 -0
  70. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/approval_workflow.py +0 -0
  71. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/audit_logger.py +0 -0
  72. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/permission_controller.py +0 -0
  73. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/permission_rules.py +0 -0
  74. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/security/security_manager.py +0 -0
  75. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/core/signal_handler.py +0 -0
  76. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/events/__init__.py +0 -0
  77. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/events/emitter.py +0 -0
  78. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/events/jsonl_writer.py +0 -0
  79. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/events/schema.py +0 -0
  80. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/mcp/__init__.py +0 -0
  81. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/mcp/client.py +0 -0
  82. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/mcp/manager.py +0 -0
  83. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/__init__.py +0 -0
  84. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/azure.py +0 -0
  85. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/base.py +0 -0
  86. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/bedrock.py +0 -0
  87. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/claude.py +0 -0
  88. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/claude_code.py +0 -0
  89. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/claude_credentials.py +0 -0
  90. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/cohere.py +0 -0
  91. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/digitalocean.py +0 -0
  92. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/factory.py +0 -0
  93. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/gemini.py +0 -0
  94. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/mistral.py +0 -0
  95. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/ollama.py +0 -0
  96. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/openai.py +0 -0
  97. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/openai_compatible.py +0 -0
  98. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/openrouter.py +0 -0
  99. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/perplexity.py +0 -0
  100. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/vertex.py +0 -0
  101. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/providers/xai.py +0 -0
  102. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/__init__.py +0 -0
  103. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/__init__.py +0 -0
  104. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/app.py +0 -0
  105. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/cli.py +0 -0
  106. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/models.py +0 -0
  107. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/sources.py +0 -0
  108. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/tui/fleet/widgets.py +0 -0
  109. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/ui/__init__.py +0 -0
  110. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/ui/cancellation_handler.py +0 -0
  111. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/ui/progress_indicator.py +0 -0
  112. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/ui/streaming_display.py +0 -0
  113. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/ui/turn_activity.py +0 -0
  114. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/utils/__init__.py +0 -0
  115. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/utils/error_handler.py +0 -0
  116. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/utils/errors.py +0 -0
  117. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer/utils/retry.py +0 -0
  118. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer_cli.egg-info/SOURCES.txt +0 -0
  119. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
  120. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer_cli.egg-info/entry_points.txt +0 -0
  121. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer_cli.egg-info/requires.txt +0 -0
  122. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/omnimancer_cli.egg-info/top_level.txt +0 -0
  123. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/setup.cfg +0 -0
  124. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_approval_integration_compatibility.py +0 -0
  125. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_approval_preview_format.py +0 -0
  126. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_approval_remember.py +0 -0
  127. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_approval_system.py +0 -0
  128. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_azure_provider.py +0 -0
  129. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_bedrock_provider.py +0 -0
  130. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_claude_code_provider.py +0 -0
  131. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_claude_credentials.py +0 -0
  132. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_claude_provider.py +0 -0
  133. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_cohere_provider.py +0 -0
  134. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_command_approval_security.py +0 -0
  135. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_config_comprehensive.py +0 -0
  136. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_config_migration_passthrough.py +0 -0
  137. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_conversation_history.py +0 -0
  138. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_conversation_manager.py +0 -0
  139. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_core_engine.py +0 -0
  140. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_digitalocean_provider.py +0 -0
  141. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_enhanced_model_info.py +0 -0
  142. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_env_loader.py +0 -0
  143. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_error_handling_robustness.py +0 -0
  144. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_execution_timeout_errors.py +0 -0
  145. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_fallback_manager.py +0 -0
  146. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_fixtures.py +0 -0
  147. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_gemini_provider.py +0 -0
  148. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_hooks.py +0 -0
  149. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_httpx_compatibility.py +0 -0
  150. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_import_validation.py +0 -0
  151. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_interface_smoke_tests.py +0 -0
  152. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_mcp_integration.py +0 -0
  153. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_mcp_integration_layer.py +0 -0
  154. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_mcp_management_commands.py +0 -0
  155. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_mistral_provider.py +0 -0
  156. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_models.py +0 -0
  157. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_ollama_provider.py +0 -0
  158. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openai_compatible_provider.py +0 -0
  159. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openai_context_fit.py +0 -0
  160. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openai_provider_tools.py +0 -0
  161. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openai_request_timeout.py +0 -0
  162. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openai_streaming.py +0 -0
  163. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_openrouter_provider.py +0 -0
  164. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_permission_rules.py +0 -0
  165. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_perplexity_provider.py +0 -0
  166. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_program_executor.py +0 -0
  167. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_aliases.py +0 -0
  168. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_base.py +0 -0
  169. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_capability_consistency.py +0 -0
  170. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_discovery.py +0 -0
  171. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_integration.py +0 -0
  172. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_provider_registry.py +0 -0
  173. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_rate_limit_fallback.py +0 -0
  174. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_retry.py +0 -0
  175. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_security_validation.py +0 -0
  176. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_system_integration.py +0 -0
  177. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_system_prompts.py +0 -0
  178. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_tool_calling_integration.py +0 -0
  179. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_tool_handler.py +0 -0
  180. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_vertex_provider.py +0 -0
  181. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_web_request_markers.py +0 -0
  182. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/tests/test_workflows_comprehensive.py +0 -0
  183. {omnimancer_cli-0.2.26 → omnimancer_cli-0.2.27}/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.26
3
+ Version: 0.2.27
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
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omnimancer-cli
3
- Version: 0.2.26
3
+ Version: 0.2.27
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "omnimancer-cli"
7
- version = "0.2.26"
7
+ version = "0.2.27"
8
8
  description = "A unified command-line interface for multiple AI language models"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"