omnimancer-cli 0.2.13__tar.gz → 0.2.19__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 (156) hide show
  1. {omnimancer_cli-0.2.13/omnimancer_cli.egg-info → omnimancer_cli-0.2.19}/PKG-INFO +1 -1
  2. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/headless.py +21 -0
  3. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/tool_handler.py +12 -0
  4. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/file_system_manager.py +49 -4
  5. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/program_executor.py +9 -0
  6. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent_managers.py +24 -0
  7. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/models.py +23 -0
  8. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/security/permission_controller.py +16 -1
  9. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/security/security_manager.py +11 -0
  10. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/azure.py +7 -3
  11. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/mistral.py +7 -3
  12. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/openai.py +6 -7
  13. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/openrouter.py +11 -4
  14. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/xai.py +7 -3
  15. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19/omnimancer_cli.egg-info}/PKG-INFO +1 -1
  16. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/pyproject.toml +1 -1
  17. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_models.py +114 -65
  18. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_openrouter_provider.py +97 -0
  19. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_tool_handler.py +15 -0
  20. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_xai_provider.py +5 -3
  21. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/LICENSE +0 -0
  22. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/README.md +0 -0
  23. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/__init__.py +0 -0
  24. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/__main__.py +0 -0
  25. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/__init__.py +0 -0
  26. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/agent_loop.py +0 -0
  27. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/approval_formatter.py +0 -0
  28. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/approval_integration.py +0 -0
  29. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/approval_prompt.py +0 -0
  30. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/command_dispatch.py +0 -0
  31. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/commands.py +0 -0
  32. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/completion.py +0 -0
  33. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/display.py +0 -0
  34. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/dynamic_commands.py +0 -0
  35. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/handlers/__init__.py +0 -0
  36. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/interface.py +0 -0
  37. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/subagent.py +0 -0
  38. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/cli/system_prompts.py +0 -0
  39. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/__init__.py +0 -0
  40. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/__init__.py +0 -0
  41. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/approval_context.py +0 -0
  42. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/approval_interface.py +0 -0
  43. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/approval_manager.py +0 -0
  44. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/config.py +0 -0
  45. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/read_before_write_errors.py +0 -0
  46. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/status_core.py +0 -0
  47. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/status_manager.py +0 -0
  48. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/tool_definitions.py +0 -0
  49. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/types.py +0 -0
  50. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/web_client.py +0 -0
  51. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
  52. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent_engine.py +0 -0
  53. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/agent_mode_manager.py +0 -0
  54. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/chat_manager.py +0 -0
  55. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/config_manager.py +0 -0
  56. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/config_migration.py +0 -0
  57. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/config_validator.py +0 -0
  58. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/conversation_manager.py +0 -0
  59. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/engine.py +0 -0
  60. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/env_loader.py +0 -0
  61. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/fallback_manager.py +0 -0
  62. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/health_monitor.py +0 -0
  63. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/history_manager.py +0 -0
  64. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/hooks.py +0 -0
  65. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/mcp_integration_layer.py +0 -0
  66. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/provider_capabilities.py +0 -0
  67. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/provider_initializer.py +0 -0
  68. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/provider_registry.py +0 -0
  69. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/rate_limit_fallback.py +0 -0
  70. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/security/__init__.py +0 -0
  71. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/security/approval_workflow.py +0 -0
  72. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/security/audit_logger.py +0 -0
  73. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/security/permission_rules.py +0 -0
  74. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/security/sandbox_manager.py +0 -0
  75. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/core/signal_handler.py +0 -0
  76. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/mcp/__init__.py +0 -0
  77. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/mcp/client.py +0 -0
  78. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/mcp/manager.py +0 -0
  79. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/__init__.py +0 -0
  80. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/base.py +0 -0
  81. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/bedrock.py +0 -0
  82. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/claude.py +0 -0
  83. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/claude_code.py +0 -0
  84. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/claude_credentials.py +0 -0
  85. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/cohere.py +0 -0
  86. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/digitalocean.py +0 -0
  87. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/factory.py +0 -0
  88. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/gemini.py +0 -0
  89. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/ollama.py +0 -0
  90. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/perplexity.py +0 -0
  91. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/providers/vertex.py +0 -0
  92. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/ui/__init__.py +0 -0
  93. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/ui/cancellation_handler.py +0 -0
  94. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/ui/progress_indicator.py +0 -0
  95. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/ui/streaming_display.py +0 -0
  96. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/utils/__init__.py +0 -0
  97. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/utils/error_handler.py +0 -0
  98. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/utils/errors.py +0 -0
  99. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer/utils/retry.py +0 -0
  100. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer_cli.egg-info/SOURCES.txt +0 -0
  101. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
  102. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer_cli.egg-info/entry_points.txt +0 -0
  103. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer_cli.egg-info/requires.txt +0 -0
  104. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/omnimancer_cli.egg-info/top_level.txt +0 -0
  105. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/setup.cfg +0 -0
  106. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_approval_integration_compatibility.py +0 -0
  107. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_approval_preview_format.py +0 -0
  108. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_approval_remember.py +0 -0
  109. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_approval_system.py +0 -0
  110. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_azure_provider.py +0 -0
  111. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_bedrock_provider.py +0 -0
  112. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_claude_code_provider.py +0 -0
  113. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_claude_credentials.py +0 -0
  114. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_claude_provider.py +0 -0
  115. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_cohere_provider.py +0 -0
  116. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_command_approval_security.py +0 -0
  117. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_config_comprehensive.py +0 -0
  118. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_conversation_history.py +0 -0
  119. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_conversation_manager.py +0 -0
  120. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_core_engine.py +0 -0
  121. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_digitalocean_provider.py +0 -0
  122. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_enhanced_model_info.py +0 -0
  123. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_env_loader.py +0 -0
  124. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_error_handling_robustness.py +0 -0
  125. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_execution_timeout_errors.py +0 -0
  126. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_fallback_manager.py +0 -0
  127. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_fixtures.py +0 -0
  128. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_gemini_provider.py +0 -0
  129. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_hooks.py +0 -0
  130. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_httpx_compatibility.py +0 -0
  131. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_import_validation.py +0 -0
  132. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_interface_smoke_tests.py +0 -0
  133. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_mcp_integration.py +0 -0
  134. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_mcp_integration_layer.py +0 -0
  135. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_mcp_management_commands.py +0 -0
  136. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_mistral_provider.py +0 -0
  137. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_ollama_provider.py +0 -0
  138. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_openai_context_fit.py +0 -0
  139. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_openai_provider_tools.py +0 -0
  140. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_permission_rules.py +0 -0
  141. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_perplexity_provider.py +0 -0
  142. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_program_executor.py +0 -0
  143. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_provider_base.py +0 -0
  144. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_provider_capability_consistency.py +0 -0
  145. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_provider_discovery.py +0 -0
  146. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_provider_integration.py +0 -0
  147. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_provider_registry.py +0 -0
  148. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_rate_limit_fallback.py +0 -0
  149. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_retry.py +0 -0
  150. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_security_validation.py +0 -0
  151. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_system_integration.py +0 -0
  152. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_system_prompts.py +0 -0
  153. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_tool_calling_integration.py +0 -0
  154. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_vertex_provider.py +0 -0
  155. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_web_request_markers.py +0 -0
  156. {omnimancer_cli-0.2.13 → omnimancer_cli-0.2.19}/tests/test_workflows_comprehensive.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omnimancer-cli
3
- Version: 0.2.13
3
+ Version: 0.2.19
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
@@ -255,6 +255,26 @@ class HeadlessRunner:
255
255
  if enhanced is not None:
256
256
  enhanced.set_approval_callback(_approve)
257
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
+
258
278
  async def run(self, prompt: str) -> int:
259
279
  agent_engine = getattr(self._engine, "agent_engine", None)
260
280
  if not agent_engine:
@@ -263,6 +283,7 @@ class HeadlessRunner:
263
283
 
264
284
  if self._no_approval:
265
285
  self._enable_auto_approval(agent_engine)
286
+ self._enable_full_trust(agent_engine)
266
287
 
267
288
  model = self._engine.config_manager.get_config().default_provider or "unknown"
268
289
  self._emitter.emit_init(model)
@@ -150,6 +150,18 @@ class ToolHandler:
150
150
  return list(CODING_AGENT_TOOLS)
151
151
 
152
152
  async def execute_tool_call(self, tool_call: ToolCall) -> ToolResult:
153
+ # ToolCall normalizes JSON-string arguments on construction; anything
154
+ # still not a dict here is malformed — tell the model instead of
155
+ # crashing on args.get(...) inside a handler.
156
+ if not isinstance(tool_call.arguments, dict):
157
+ return ToolResult(
158
+ content="",
159
+ error=(
160
+ f"Malformed arguments for {tool_call.name}: expected a "
161
+ f"JSON object, got: {str(tool_call.arguments)[:200]}. "
162
+ "Re-issue the call with the parameters as a JSON object."
163
+ ),
164
+ )
153
165
  try:
154
166
  # Read and Edit need post-processing / multi-step logic.
155
167
  if tool_call.name in _EDIT_TOOLS:
@@ -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:
@@ -242,14 +250,51 @@ class FileSystemManager:
242
250
  """
243
251
  Simple boolean check for file existence.
244
252
 
253
+ Performs a lightweight path-validation check followed by an async
254
+ filesystem probe. Unlike :meth:`check_file_exists`, this method does
255
+ *not* invoke the full security-audit pipeline (permission logging, risk
256
+ assessment, approval workflow). It is intended for high-frequency
257
+ callers (e.g. batch operations) that need a fast boolean answer.
258
+
259
+ .. warning::
260
+ This method enforces path-level access controls — hard-restricted
261
+ system paths (``/etc``, ``~/.ssh``, cloud-credential stores, etc.)
262
+ and sensitive-name patterns are still blocked via
263
+ :meth:`~omnimancer.core.security.permission_controller.PermissionController.validate_path_access`.
264
+ However, it skips the per-operation audit-log entries produced by
265
+ :meth:`~omnimancer.core.security.SecurityManager.secure_file_access`.
266
+ Do not use this method where per-access audit trails are required.
267
+
245
268
  Args:
246
- path: Path to check for existence
269
+ path: Path to check for existence.
247
270
 
248
271
  Returns:
249
- True if file exists and is accessible, False otherwise
272
+ True if the path passes security validation and exists on the
273
+ filesystem; False if the path is security-denied or does not exist.
250
274
  """
251
- result = await self.check_file_exists(path)
252
- return result["exists"] and result["error"] is None
275
+ try:
276
+ resolved = Path(path).resolve()
277
+ except (OSError, ValueError) as e:
278
+ logger.warning("file_exists: invalid path %r: %s", path, e)
279
+ return False
280
+
281
+ # Validate path against security policy without invoking the expensive
282
+ # audit pipeline. This blocks hard-restricted system paths and
283
+ # sensitive-name patterns (e.g. /etc, ~/.ssh, *.db) the same way the
284
+ # full pipeline would.
285
+ if not self.security.permissions.validate_path_access(str(resolved), "read"):
286
+ logger.warning(
287
+ "file_exists: access denied by security policy for %s", resolved
288
+ )
289
+ return False
290
+
291
+ try:
292
+ exists = bool(await aiofiles.os.path.exists(resolved))
293
+ logger.debug("file_exists: %s → %s", resolved, exists)
294
+ return exists
295
+ except OSError as e:
296
+ logger.warning("file_exists: OS error checking %s: %s", resolved, e)
297
+ return False
253
298
 
254
299
  async def write_file_with_confirmation(
255
300
  self,
@@ -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
@@ -159,6 +159,10 @@ class ProgramExecutor(BaseManager):
159
159
  }
160
160
  self.timeout_seconds = self.default_config.timeout_seconds
161
161
 
162
+ # Full-trust mode (unattended headless runs where an outer sandbox is
163
+ # the security boundary) — see set_full_trust().
164
+ self.full_trust = False
165
+
162
166
  async def execute_operation(self, operation: Operation) -> OperationResult:
163
167
  """Execute command operation using enhanced executor.
164
168
 
@@ -261,6 +265,22 @@ class ProgramExecutor(BaseManager):
261
265
  """Terminate a running command."""
262
266
  return await self.enhanced_executor.terminate_command(execution_id)
263
267
 
268
+ def set_full_trust(self, enabled: bool) -> None:
269
+ """Enable full-trust mode for unattended runs.
270
+
271
+ The caller (e.g. a headless agent inside a container) is the security
272
+ boundary: the forbidden-command list and argument sanitization stop
273
+ blocking, and the default per-command timeout is raised so real build/
274
+ test runs can finish. Explicit per-operation timeouts are still honored.
275
+ """
276
+ self.full_trust = enabled
277
+ if enabled:
278
+ self.timeout_seconds = 600
279
+ self.default_config.timeout_seconds = 600
280
+ validator = getattr(self.enhanced_executor, "validator", None)
281
+ if validator is not None:
282
+ validator.full_trust = enabled
283
+
264
284
  def _validate_command(self, command: str) -> bool:
265
285
  """
266
286
  Validate command for backward compatibility with tests.
@@ -278,6 +298,10 @@ class ProgramExecutor(BaseManager):
278
298
  Security is provided through the approval system's risk assessment
279
299
  and user confirmation. Only explicitly dangerous commands are blocked.
280
300
  """
301
+ # Full trust: forbidden-command blocking is disabled.
302
+ if self.full_trust:
303
+ return True
304
+
281
305
  # Extract the base command (first word)
282
306
  base_command = command.strip().split()[0] if command.strip() else ""
283
307
 
@@ -113,6 +113,29 @@ class ToolCall:
113
113
  # Provider-assigned call id (OpenAI protocol pairs results to calls by id).
114
114
  id: Optional[str] = None
115
115
 
116
+ def __post_init__(self) -> None:
117
+ """Normalize arguments that arrive as a JSON-encoded string.
118
+
119
+ OpenAI-protocol servers send ``function.arguments`` as a JSON string,
120
+ and some models double-encode it. A value that doesn't decode to an
121
+ object is left as-is so the tool handler can report it to the model.
122
+ """
123
+ import json
124
+
125
+ if self.arguments is None:
126
+ self.arguments = {}
127
+ return
128
+ decoded: Any = self.arguments
129
+ for _ in range(2):
130
+ if not isinstance(decoded, str):
131
+ break
132
+ try:
133
+ decoded = json.loads(decoded)
134
+ except ValueError:
135
+ return
136
+ if isinstance(decoded, dict):
137
+ self.arguments = decoded
138
+
116
139
 
117
140
  @dataclass
118
141
  class ToolResultRecord:
@@ -46,6 +46,11 @@ class PermissionController:
46
46
  self.allowed_commands = self._get_default_allowed_commands()
47
47
  self.permission_rules = self._get_default_permission_rules()
48
48
 
49
+ # Full-trust mode (unattended headless runs where an outer sandbox is
50
+ # the security boundary): sensitive name patterns and the command
51
+ # allowlist stop blocking. Hard-restricted paths still deny.
52
+ self.full_trust = False
53
+
49
54
  # Initialize approval memory storage
50
55
  self._approval_memory: Dict[str, Dict[str, Any]] = {}
51
56
 
@@ -193,11 +198,16 @@ class PermissionController:
193
198
  # Normalize path
194
199
  normalized_path = str(Path(path).resolve())
195
200
 
196
- # Hard-restricted paths are denied regardless of approval.
201
+ # Hard-restricted paths are denied regardless of approval —
202
+ # including full-trust mode.
197
203
  for restricted in self.restricted_paths:
198
204
  if self._path_matches_pattern(normalized_path, restricted):
199
205
  return False
200
206
 
207
+ # Full trust treats every operation as explicitly allowed.
208
+ if self.full_trust:
209
+ allow_sensitive = True
210
+
201
211
  # Sensitive name patterns are denied unless explicitly allowed.
202
212
  if not allow_sensitive:
203
213
  for pattern in self.sensitive_patterns:
@@ -238,6 +248,11 @@ class PermissionController:
238
248
 
239
249
  def validate_command(self, command: str) -> bool:
240
250
  """Validate if command execution is allowed."""
251
+ # Full trust: the allowlist and pattern checks stop blocking — the
252
+ # outer sandbox is the security boundary.
253
+ if self.full_trust:
254
+ return True
255
+
241
256
  # Extract base command (first word)
242
257
  base_command = command.strip().split()[0]
243
258
 
@@ -327,6 +327,17 @@ class SecurityManager:
327
327
  result["stderr"] = f"Security manager error: {str(e)}"
328
328
  return result
329
329
 
330
+ def set_full_trust(self, enabled: bool) -> None:
331
+ """Enable/disable full-trust mode on the permission controller.
332
+
333
+ Full trust is for unattended runs (e.g. headless agents inside a
334
+ container) where the caller is the security boundary: sensitive name
335
+ patterns and the command allowlist stop blocking operations. The
336
+ hard-restricted system paths in PermissionController.restricted_paths
337
+ still deny.
338
+ """
339
+ self.permissions.full_trust = enabled
340
+
330
341
  async def secure_file_access(
331
342
  self,
332
343
  file_path: str,
@@ -363,19 +363,23 @@ class AzureProvider(BaseProvider):
363
363
 
364
364
  if choices and len(choices) > 0:
365
365
  message = choices[0].get("message", {})
366
- content = message.get("content", "")
366
+ # Tool-call responses carry "content": null — coalesce to "".
367
+ content = message.get("content") or ""
367
368
  usage = data.get("usage", {})
368
369
 
369
- # Extract tool calls if present
370
+ # Extract tool calls if present. The wire protocol sends
371
+ # function.arguments as a JSON string; ToolCall normalizes
372
+ # it to a dict on construction.
370
373
  tool_calls = []
371
374
  if "tool_calls" in message:
372
- for tool_call in message["tool_calls"]:
375
+ for i, tool_call in enumerate(message["tool_calls"]):
373
376
  if tool_call.get("type") == "function":
374
377
  function = tool_call.get("function", {})
375
378
  tool_calls.append(
376
379
  ToolCall(
377
380
  name=function.get("name", ""),
378
381
  arguments=function.get("arguments", {}),
382
+ id=tool_call.get("id") or f"call_{i}",
379
383
  )
380
384
  )
381
385
 
@@ -324,19 +324,23 @@ class MistralProvider(BaseProvider):
324
324
 
325
325
  if choices and len(choices) > 0:
326
326
  message = choices[0].get("message", {})
327
- content = message.get("content", "")
327
+ # Tool-call responses carry "content": null — coalesce to "".
328
+ content = message.get("content") or ""
328
329
  usage = data.get("usage", {})
329
330
 
330
- # Extract tool calls if present
331
+ # Extract tool calls if present. The wire protocol sends
332
+ # function.arguments as a JSON string; ToolCall normalizes
333
+ # it to a dict on construction.
331
334
  tool_calls = []
332
335
  if "tool_calls" in message:
333
- for tool_call in message["tool_calls"]:
336
+ for i, tool_call in enumerate(message["tool_calls"]):
334
337
  if tool_call.get("type") == "function":
335
338
  function = tool_call.get("function", {})
336
339
  tool_calls.append(
337
340
  ToolCall(
338
341
  name=function.get("name", ""),
339
342
  arguments=function.get("arguments", {}),
343
+ id=tool_call.get("id") or f"call_{i}",
340
344
  )
341
345
  )
342
346
 
@@ -332,7 +332,8 @@ class OpenAIProvider(BaseProvider):
332
332
 
333
333
  if choices and len(choices) > 0:
334
334
  message = choices[0].get("message", {})
335
- content = message.get("content", "")
335
+ # Tool-call responses carry "content": null — coalesce to "".
336
+ content = message.get("content") or ""
336
337
  usage = data.get("usage", {})
337
338
 
338
339
  return ChatResponse(
@@ -462,18 +463,16 @@ class OpenAIProvider(BaseProvider):
462
463
  tool_calls = None
463
464
  raw_tool_calls = msg.get("tool_calls")
464
465
  if raw_tool_calls:
465
- import json
466
-
467
466
  tool_calls = []
468
467
  for i, tc in enumerate(raw_tool_calls):
469
468
  func = tc.get("function", {})
470
- args = func.get("arguments", "{}")
471
- if isinstance(args, str):
472
- args = json.loads(args)
473
469
  tool_calls.append(
474
470
  ToolCall(
471
+ # ToolCall normalizes JSON-string arguments
472
+ # (including double-encoded ones) on
473
+ # construction.
475
474
  name=func.get("name", ""),
476
- arguments=args,
475
+ arguments=func.get("arguments", {}),
477
476
  # Some OpenAI-compatible servers omit ids;
478
477
  # synthesize one so results can pair to calls.
479
478
  id=tc.get("id") or f"call_{i}",
@@ -377,7 +377,9 @@ class OpenRouterProvider(BaseProvider):
377
377
 
378
378
  if choices and len(choices) > 0:
379
379
  message = choices[0].get("message", {})
380
- content = message.get("content", "")
380
+ # Tool-call responses carry "content": null — coalesce to ""
381
+ # so the fallback-warning concatenation below can't TypeError.
382
+ content = message.get("content") or ""
381
383
  usage = data.get("usage", {})
382
384
 
383
385
  # Extract model used (OpenRouter may route to different models)
@@ -446,19 +448,24 @@ class OpenRouterProvider(BaseProvider):
446
448
 
447
449
  if choices and len(choices) > 0:
448
450
  message = choices[0].get("message", {})
449
- content = message.get("content", "")
451
+ # Tool-call responses carry "content": null — coalesce to ""
452
+ # so the fallback-warning concatenation below can't TypeError.
453
+ content = message.get("content") or ""
450
454
  usage = data.get("usage", {})
451
455
 
452
- # Extract tool calls if present
456
+ # Extract tool calls if present. The wire protocol sends
457
+ # function.arguments as a JSON string; ToolCall normalizes
458
+ # it to a dict on construction.
453
459
  tool_calls = []
454
460
  if "tool_calls" in message:
455
- for tool_call in message["tool_calls"]:
461
+ for i, tool_call in enumerate(message["tool_calls"]):
456
462
  if tool_call.get("type") == "function":
457
463
  function = tool_call.get("function", {})
458
464
  tool_calls.append(
459
465
  ToolCall(
460
466
  name=function.get("name", ""),
461
467
  arguments=function.get("arguments", {}),
468
+ id=tool_call.get("id") or f"call_{i}",
462
469
  )
463
470
  )
464
471
 
@@ -327,19 +327,23 @@ class XAIProvider(BaseProvider):
327
327
 
328
328
  if choices and len(choices) > 0:
329
329
  message = choices[0].get("message", {})
330
- content = message.get("content", "")
330
+ # Tool-call responses carry "content": null — coalesce to "".
331
+ content = message.get("content") or ""
331
332
  usage = data.get("usage", {})
332
333
 
333
- # Extract tool calls if present
334
+ # Extract tool calls if present. The wire protocol sends
335
+ # function.arguments as a JSON string; ToolCall normalizes
336
+ # it to a dict on construction.
334
337
  tool_calls = []
335
338
  if "tool_calls" in message:
336
- for tool_call in message["tool_calls"]:
339
+ for i, tool_call in enumerate(message["tool_calls"]):
337
340
  if tool_call.get("type") == "function":
338
341
  function = tool_call.get("function", {})
339
342
  tool_calls.append(
340
343
  ToolCall(
341
344
  name=function.get("name", ""),
342
345
  arguments=function.get("arguments", {}),
346
+ id=tool_call.get("id") or f"call_{i}",
343
347
  )
344
348
  )
345
349
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omnimancer-cli
3
- Version: 0.2.13
3
+ Version: 0.2.19
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.13"
7
+ version = "0.2.19"
8
8
  description = "A unified command-line interface for multiple AI language models"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -591,68 +591,117 @@ class TestStreamEvent:
591
591
 
592
592
  event = StreamEvent(type=StreamEventType.ERROR, error="Connection lost")
593
593
  assert event.error == "Connection lost"
594
-
595
-
596
- class TestParseDescribedToolCalls:
597
- """parse_described_tool_calls recovers calls a model emitted as text.
598
-
599
- Weak models mimic the "[Called tools: ...]" history notation instead of
600
- issuing native tool calls; the parser turns that text back into ToolCall
601
- objects so the agent loop can keep going.
602
- """
603
-
604
- def test_round_trips_describe_output(self):
605
- from omnimancer.core.models import (
606
- ToolCall,
607
- describe_tool_calls,
608
- parse_described_tool_calls,
609
- )
610
-
611
- calls = [
612
- ToolCall(name="Grep", arguments={"pattern": "auth", "glob": "*.ts"}),
613
- ToolCall(name="Read", arguments={"file_path": "/src/main.py"}),
614
- ]
615
- parsed = parse_described_tool_calls(describe_tool_calls(calls))
616
-
617
- assert [(c.name, c.arguments) for c in parsed] == [
618
- (c.name, c.arguments) for c in calls
619
- ]
620
-
621
- def test_plain_text_returns_empty(self):
622
- from omnimancer.core.models import parse_described_tool_calls
623
-
624
- assert parse_described_tool_calls("The auth flow looks correct.") == []
625
- assert parse_described_tool_calls("") == []
626
- assert parse_described_tool_calls(None) == []
627
-
628
- def test_nested_json_with_braces_and_commas(self):
629
- from omnimancer.core.models import parse_described_tool_calls
630
-
631
- text = (
632
- '[Called tools: Grep({"glob": "*.{ts,tsx}", '
633
- '"output_mode": "files_with_matches", '
634
- '"pattern": "AUTH_SESSION_KEY|automarketer_auth_session"})]'
635
- )
636
- parsed = parse_described_tool_calls(text)
637
-
638
- assert len(parsed) == 1
639
- assert parsed[0].name == "Grep"
640
- assert parsed[0].arguments["glob"] == "*.{ts,tsx}"
641
-
642
- def test_marker_embedded_in_other_text(self):
643
- from omnimancer.core.models import parse_described_tool_calls
644
-
645
- text = (
646
- "Let me search for that.\n"
647
- '[Called tools: Bash({"command": "git log --oneline -5"})]'
648
- )
649
- parsed = parse_described_tool_calls(text)
650
-
651
- assert len(parsed) == 1
652
- assert parsed[0].name == "Bash"
653
- assert parsed[0].arguments == {"command": "git log --oneline -5"}
654
-
655
- def test_malformed_json_is_skipped(self):
656
- from omnimancer.core.models import parse_described_tool_calls
657
-
658
- assert parse_described_tool_calls("[Called tools: Grep({'bad': json})]") == []
594
+
595
+
596
+ class TestParseDescribedToolCalls:
597
+ """parse_described_tool_calls recovers calls a model emitted as text.
598
+
599
+ Weak models mimic the "[Called tools: ...]" history notation instead of
600
+ issuing native tool calls; the parser turns that text back into ToolCall
601
+ objects so the agent loop can keep going.
602
+ """
603
+
604
+ def test_round_trips_describe_output(self):
605
+ from omnimancer.core.models import (
606
+ ToolCall,
607
+ describe_tool_calls,
608
+ parse_described_tool_calls,
609
+ )
610
+
611
+ calls = [
612
+ ToolCall(name="Grep", arguments={"pattern": "auth", "glob": "*.ts"}),
613
+ ToolCall(name="Read", arguments={"file_path": "/src/main.py"}),
614
+ ]
615
+ parsed = parse_described_tool_calls(describe_tool_calls(calls))
616
+
617
+ assert [(c.name, c.arguments) for c in parsed] == [
618
+ (c.name, c.arguments) for c in calls
619
+ ]
620
+
621
+ def test_plain_text_returns_empty(self):
622
+ from omnimancer.core.models import parse_described_tool_calls
623
+
624
+ assert parse_described_tool_calls("The auth flow looks correct.") == []
625
+ assert parse_described_tool_calls("") == []
626
+ assert parse_described_tool_calls(None) == []
627
+
628
+ def test_nested_json_with_braces_and_commas(self):
629
+ from omnimancer.core.models import parse_described_tool_calls
630
+
631
+ text = (
632
+ '[Called tools: Grep({"glob": "*.{ts,tsx}", '
633
+ '"output_mode": "files_with_matches", '
634
+ '"pattern": "AUTH_SESSION_KEY|automarketer_auth_session"})]'
635
+ )
636
+ parsed = parse_described_tool_calls(text)
637
+
638
+ assert len(parsed) == 1
639
+ assert parsed[0].name == "Grep"
640
+ assert parsed[0].arguments["glob"] == "*.{ts,tsx}"
641
+
642
+ def test_marker_embedded_in_other_text(self):
643
+ from omnimancer.core.models import parse_described_tool_calls
644
+
645
+ text = (
646
+ "Let me search for that.\n"
647
+ '[Called tools: Bash({"command": "git log --oneline -5"})]'
648
+ )
649
+ parsed = parse_described_tool_calls(text)
650
+
651
+ assert len(parsed) == 1
652
+ assert parsed[0].name == "Bash"
653
+ assert parsed[0].arguments == {"command": "git log --oneline -5"}
654
+
655
+ def test_malformed_json_is_skipped(self):
656
+ from omnimancer.core.models import parse_described_tool_calls
657
+
658
+ assert parse_described_tool_calls("[Called tools: Grep({'bad': json})]") == []
659
+
660
+
661
+ class TestToolCallArgumentNormalization:
662
+ """ToolCall normalizes arguments that arrive as JSON-encoded strings.
663
+
664
+ OpenAI-protocol servers send function.arguments as a JSON string (and
665
+ some models double-encode it). Providers that passed the raw value
666
+ through crashed the tool handler with "'str' object has no attribute
667
+ 'get'".
668
+ """
669
+
670
+ def test_json_string_arguments_become_dict(self):
671
+ from omnimancer.core.models import ToolCall
672
+
673
+ tc = ToolCall(name="Glob", arguments='{"pattern": "**/*.py"}')
674
+ assert tc.arguments == {"pattern": "**/*.py"}
675
+
676
+ def test_double_encoded_arguments_become_dict(self):
677
+ import json
678
+
679
+ from omnimancer.core.models import ToolCall
680
+
681
+ double = json.dumps(json.dumps({"pattern": "TODO"}))
682
+ tc = ToolCall(name="Grep", arguments=double)
683
+ assert tc.arguments == {"pattern": "TODO"}
684
+
685
+ def test_none_arguments_become_empty_dict(self):
686
+ from omnimancer.core.models import ToolCall
687
+
688
+ tc = ToolCall(name="Read", arguments=None)
689
+ assert tc.arguments == {}
690
+
691
+ def test_dict_arguments_unchanged(self):
692
+ from omnimancer.core.models import ToolCall
693
+
694
+ tc = ToolCall(name="Read", arguments={"file_path": "/src/main.py"})
695
+ assert tc.arguments == {"file_path": "/src/main.py"}
696
+
697
+ def test_unparseable_string_left_for_handler_to_report(self):
698
+ from omnimancer.core.models import ToolCall
699
+
700
+ tc = ToolCall(name="Glob", arguments="not json at all")
701
+ assert tc.arguments == "not json at all"
702
+
703
+ def test_non_object_json_left_for_handler_to_report(self):
704
+ from omnimancer.core.models import ToolCall
705
+
706
+ tc = ToolCall(name="Glob", arguments="[1, 2, 3]")
707
+ assert tc.arguments == "[1, 2, 3]"