talkops-opscloud 0.3.0b1__py3-none-any.whl

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 (309) hide show
  1. opscloud/__init__.py +5 -0
  2. opscloud/__main__.py +6 -0
  3. opscloud/_constants.py +53 -0
  4. opscloud/_debug.py +145 -0
  5. opscloud/_debug_buffer.py +250 -0
  6. opscloud/_textual_patches.py +367 -0
  7. opscloud/_version.py +15 -0
  8. opscloud/agent/__init__.py +27 -0
  9. opscloud/agent/config.py +168 -0
  10. opscloud/agent/factory.py +1103 -0
  11. opscloud/approval_mode.py +44 -0
  12. opscloud/backend/__init__.py +49 -0
  13. opscloud/backend/composite.py +89 -0
  14. opscloud/backend/local.py +183 -0
  15. opscloud/backend/registry.py +79 -0
  16. opscloud/backend/sandbox_config.py +107 -0
  17. opscloud/backend/sandbox_factory.py +432 -0
  18. opscloud/built_in_skills/remember/SKILL.md +124 -0
  19. opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
  20. opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
  21. opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
  22. opscloud/cli/__init__.py +1 -0
  23. opscloud/cli/commands/__init__.py +15 -0
  24. opscloud/cli/commands/auth.py +45 -0
  25. opscloud/cli/commands/doctor.py +100 -0
  26. opscloud/cli/commands/mcp.py +137 -0
  27. opscloud/cli/commands/threads.py +51 -0
  28. opscloud/cli/interactive.py +154 -0
  29. opscloud/cli/main.py +903 -0
  30. opscloud/cli/non_interactive.py +425 -0
  31. opscloud/cli/server_manager.py +58 -0
  32. opscloud/commands/__init__.py +19 -0
  33. opscloud/commands/_base.py +110 -0
  34. opscloud/commands/_guards.py +52 -0
  35. opscloud/commands/_router.py +146 -0
  36. opscloud/commands/_types.py +36 -0
  37. opscloud/commands/core/__init__.py +47 -0
  38. opscloud/commands/core/auth.py +94 -0
  39. opscloud/commands/core/bug.py +46 -0
  40. opscloud/commands/core/clear.py +106 -0
  41. opscloud/commands/core/cloud.py +185 -0
  42. opscloud/commands/core/compact.py +128 -0
  43. opscloud/commands/core/config_cmd.py +120 -0
  44. opscloud/commands/core/context.py +220 -0
  45. opscloud/commands/core/cost.py +44 -0
  46. opscloud/commands/core/doctor.py +535 -0
  47. opscloud/commands/core/effort.py +108 -0
  48. opscloud/commands/core/exit_cmd.py +41 -0
  49. opscloud/commands/core/fast.py +148 -0
  50. opscloud/commands/core/help_cmd.py +94 -0
  51. opscloud/commands/core/mcp.py +134 -0
  52. opscloud/commands/core/model.py +109 -0
  53. opscloud/commands/core/permissions.py +196 -0
  54. opscloud/commands/core/plugins.py +92 -0
  55. opscloud/commands/core/pool.py +108 -0
  56. opscloud/commands/core/resume.py +97 -0
  57. opscloud/commands/core/skills.py +178 -0
  58. opscloud/commands/power/__init__.py +51 -0
  59. opscloud/commands/power/agents.py +98 -0
  60. opscloud/commands/power/btw.py +85 -0
  61. opscloud/commands/power/copy.py +97 -0
  62. opscloud/commands/power/goal.py +548 -0
  63. opscloud/commands/power/loop.py +295 -0
  64. opscloud/commands/power/memory.py +287 -0
  65. opscloud/commands/power/review.py +152 -0
  66. opscloud/commands/power/rubric.py +287 -0
  67. opscloud/commands/power/runtime.py +321 -0
  68. opscloud/commands/power/skill_creator.py +70 -0
  69. opscloud/commands/power/skill_invoke.py +165 -0
  70. opscloud/commands/power/tasks.py +209 -0
  71. opscloud/commands/power/trace.py +117 -0
  72. opscloud/commands/power/ui_toggles.py +135 -0
  73. opscloud/commands/power/version.py +52 -0
  74. opscloud/commands/registry.py +176 -0
  75. opscloud/config/__init__.py +114 -0
  76. opscloud/config/adapters/__init__.py +5 -0
  77. opscloud/config/adapters/postgres.py +776 -0
  78. opscloud/config/adapters/sqlite.py +769 -0
  79. opscloud/config/aws.py +248 -0
  80. opscloud/config/cloud_profiles.py +437 -0
  81. opscloud/config/env_vars.py +81 -0
  82. opscloud/config/langsmith.py +319 -0
  83. opscloud/config/manifest.py +817 -0
  84. opscloud/config/metadata.py +205 -0
  85. opscloud/config/paths.py +604 -0
  86. opscloud/config/plugins.py +130 -0
  87. opscloud/config/settings.py +834 -0
  88. opscloud/config/store.py +453 -0
  89. opscloud/config/store_factory.py +97 -0
  90. opscloud/config/toml_config.py +562 -0
  91. opscloud/editor.py +173 -0
  92. opscloud/exceptions.py +65 -0
  93. opscloud/file_ops.py +217 -0
  94. opscloud/hooks/__init__.py +139 -0
  95. opscloud/hooks/env.py +44 -0
  96. opscloud/hooks/interrupt.py +121 -0
  97. opscloud/hooks/models/__init__.py +109 -0
  98. opscloud/hooks/models/adapters.py +19 -0
  99. opscloud/hooks/models/config.py +79 -0
  100. opscloud/hooks/models/domain.py +434 -0
  101. opscloud/hooks/models/transport.py +37 -0
  102. opscloud/hooks/tools.py +231 -0
  103. opscloud/input.py +48 -0
  104. opscloud/integrations/__init__.py +80 -0
  105. opscloud/integrations/base.py +153 -0
  106. opscloud/integrations/content_filter.py +119 -0
  107. opscloud/integrations/event_bus.py +480 -0
  108. opscloud/integrations/hooks.py +238 -0
  109. opscloud/integrations/identity.py +181 -0
  110. opscloud/integrations/notifications.py +134 -0
  111. opscloud/integrations/stream_bridge.py +397 -0
  112. opscloud/integrations/thread_store.py +81 -0
  113. opscloud/json_types.py +20 -0
  114. opscloud/mcp/__init__.py +57 -0
  115. opscloud/mcp/config.py +203 -0
  116. opscloud/mcp/discovery.py +229 -0
  117. opscloud/mcp/mcp_info.py +109 -0
  118. opscloud/mcp/middleware.py +102 -0
  119. opscloud/mcp/preload.py +505 -0
  120. opscloud/mcp/raw_config.py +149 -0
  121. opscloud/mcp/semantic_profiler.py +305 -0
  122. opscloud/mcp/session_manager.py +727 -0
  123. opscloud/mcp/trust.py +105 -0
  124. opscloud/memory/__init__.py +27 -0
  125. opscloud/memory/branch.py +61 -0
  126. opscloud/memory/guard.py +19 -0
  127. opscloud/memory/onboarding.py +120 -0
  128. opscloud/memory/registry.py +134 -0
  129. opscloud/memory/store.py +223 -0
  130. opscloud/middleware/__init__.py +106 -0
  131. opscloud/middleware/_repository_bounds.py +459 -0
  132. opscloud/middleware/ask_user.py +152 -0
  133. opscloud/middleware/auto_mode.py +60 -0
  134. opscloud/middleware/auto_mode_hitl.py +2434 -0
  135. opscloud/middleware/compaction.py +566 -0
  136. opscloud/middleware/configurable_model.py +233 -0
  137. opscloud/middleware/cost_tracking.py +1021 -0
  138. opscloud/middleware/glm_stall_recovery.py +109 -0
  139. opscloud/middleware/goal_criteria.py +1234 -0
  140. opscloud/middleware/goal_state_notice.py +712 -0
  141. opscloud/middleware/goal_tools.py +407 -0
  142. opscloud/middleware/headless_mcp_guard.py +88 -0
  143. opscloud/middleware/jev_model_router.py +1018 -0
  144. opscloud/middleware/local_context.py +460 -0
  145. opscloud/middleware/mcp_context.py +151 -0
  146. opscloud/middleware/mcp_middleware.py +146 -0
  147. opscloud/middleware/memory_guard.py +393 -0
  148. opscloud/middleware/model_retry.py +205 -0
  149. opscloud/middleware/registry.py +137 -0
  150. opscloud/middleware/reliable_rubric.py +709 -0
  151. opscloud/middleware/resume_state.py +91 -0
  152. opscloud/middleware/server_hooks.py +1189 -0
  153. opscloud/middleware/shell_allow_list.py +90 -0
  154. opscloud/middleware/skills.py +382 -0
  155. opscloud/middleware/subagent_artifacts.py +84 -0
  156. opscloud/middleware/subagent_telemetry.py +261 -0
  157. opscloud/middleware/subagents.py +243 -0
  158. opscloud/middleware/tool_filter.py +384 -0
  159. opscloud/middleware/unified_system_message.py +62 -0
  160. opscloud/model/__init__.py +37 -0
  161. opscloud/model/config.py +2992 -0
  162. opscloud/model/factory.py +477 -0
  163. opscloud/model/pool.py +459 -0
  164. opscloud/model/reasoning.py +282 -0
  165. opscloud/offload.py +173 -0
  166. opscloud/output.py +64 -0
  167. opscloud/plugins/__init__.py +189 -0
  168. opscloud/plugins/_json.py +22 -0
  169. opscloud/plugins/adapters/__init__.py +13 -0
  170. opscloud/plugins/adapters/agents.py +112 -0
  171. opscloud/plugins/adapters/commands.py +190 -0
  172. opscloud/plugins/adapters/mcp.py +363 -0
  173. opscloud/plugins/adapters/skills.py +132 -0
  174. opscloud/plugins/commands_cli.py +541 -0
  175. opscloud/plugins/discovery.py +512 -0
  176. opscloud/plugins/manifest.py +335 -0
  177. opscloud/plugins/marketplace.py +893 -0
  178. opscloud/plugins/models.py +334 -0
  179. opscloud/plugins/project_plugins.py +317 -0
  180. opscloud/plugins/protocol.py +31 -0
  181. opscloud/plugins/store.py +905 -0
  182. opscloud/plugins/substitution.py +122 -0
  183. opscloud/project_utils.py +145 -0
  184. opscloud/prompts/__init__.py +37 -0
  185. opscloud/prompts/system.py +483 -0
  186. opscloud/prompts/templates/system_prompt.md +166 -0
  187. opscloud/prompts/types.py +54 -0
  188. opscloud/rubrics/__init__.py +42 -0
  189. opscloud/rubrics/evaluator.py +404 -0
  190. opscloud/rubrics/evidence_extractor.py +229 -0
  191. opscloud/rubrics/generator.py +239 -0
  192. opscloud/rubrics/jev_compiler.py +175 -0
  193. opscloud/rubrics/jev_grader.py +297 -0
  194. opscloud/rubrics/middleware.py +8 -0
  195. opscloud/schema/__init__.py +107 -0
  196. opscloud/schema/interrupts.py +981 -0
  197. opscloud/security/__init__.py +91 -0
  198. opscloud/security/approval_mode.py +528 -0
  199. opscloud/security/approval_mode_source.py +230 -0
  200. opscloud/security/cli_ast_evaluator.py +552 -0
  201. opscloud/security/jev_classifier.py +651 -0
  202. opscloud/security/shell_safety.py +340 -0
  203. opscloud/security/unicode_security.py +439 -0
  204. opscloud/security/url_validation.py +160 -0
  205. opscloud/server/__init__.py +13 -0
  206. opscloud/server/_server_config.py +158 -0
  207. opscloud/server/server.py +242 -0
  208. opscloud/server/server_graph.py +93 -0
  209. opscloud/skills/__init__.py +91 -0
  210. opscloud/skills/commands.py +1099 -0
  211. opscloud/skills/invocation.py +200 -0
  212. opscloud/skills/load.py +388 -0
  213. opscloud/skills/loader.py +23 -0
  214. opscloud/skills/merge.py +65 -0
  215. opscloud/skills/registry.py +263 -0
  216. opscloud/skills/sources.py +174 -0
  217. opscloud/skills/trust.py +417 -0
  218. opscloud/state/__init__.py +142 -0
  219. opscloud/state/base.py +154 -0
  220. opscloud/state/cloud_context.py +262 -0
  221. opscloud/state/goal_channels.py +124 -0
  222. opscloud/state/goal_state_limits.py +175 -0
  223. opscloud/state/resume_state.py +58 -0
  224. opscloud/state/service_context.py +281 -0
  225. opscloud/state/session.py +957 -0
  226. opscloud/state/state_migration.py +86 -0
  227. opscloud/subagents/__init__.py +30 -0
  228. opscloud/subagents/loader.py +331 -0
  229. opscloud/subagents/subagents_parser.py +117 -0
  230. opscloud/subagents/types.py +25 -0
  231. opscloud/tools/__init__.py +36 -0
  232. opscloud/tools/catalog.py +37 -0
  233. opscloud/tools/display.py +24 -0
  234. opscloud/tools/fetch_url.py +176 -0
  235. opscloud/tools/goal_tools.py +423 -0
  236. opscloud/tools/registry.py +105 -0
  237. opscloud/tools/thread.py +32 -0
  238. opscloud/tools/web_search.py +215 -0
  239. opscloud/ui/__init__.py +119 -0
  240. opscloud/ui/app.py +5400 -0
  241. opscloud/ui/app.tcss +479 -0
  242. opscloud/ui/clipboard.py +147 -0
  243. opscloud/ui/command_registry.py +482 -0
  244. opscloud/ui/message_store.py +152 -0
  245. opscloud/ui/permission_store.py +360 -0
  246. opscloud/ui/preferences.py +113 -0
  247. opscloud/ui/prompt_manager.py +96 -0
  248. opscloud/ui/remote_client.py +505 -0
  249. opscloud/ui/textual_adapter.py +1269 -0
  250. opscloud/ui/theme.py +728 -0
  251. opscloud/ui/ui_help.py +415 -0
  252. opscloud/ui/widgets/__init__.py +383 -0
  253. opscloud/ui/widgets/_ask_user_types.py +81 -0
  254. opscloud/ui/widgets/_copy_spans.py +52 -0
  255. opscloud/ui/widgets/_inline_prompt.py +290 -0
  256. opscloud/ui/widgets/_js_eval_display.py +139 -0
  257. opscloud/ui/widgets/_links.py +175 -0
  258. opscloud/ui/widgets/_tool_stream.py +308 -0
  259. opscloud/ui/widgets/agent_selector.py +297 -0
  260. opscloud/ui/widgets/approval.py +741 -0
  261. opscloud/ui/widgets/ask_user.py +539 -0
  262. opscloud/ui/widgets/auth.py +695 -0
  263. opscloud/ui/widgets/auth_manager.py +214 -0
  264. opscloud/ui/widgets/auto_mode_notice.py +146 -0
  265. opscloud/ui/widgets/autocomplete.py +709 -0
  266. opscloud/ui/widgets/chat_input.py +805 -0
  267. opscloud/ui/widgets/cloud_selector.py +528 -0
  268. opscloud/ui/widgets/config_manager.py +652 -0
  269. opscloud/ui/widgets/debug_console.py +1269 -0
  270. opscloud/ui/widgets/devops_renderers.py +184 -0
  271. opscloud/ui/widgets/diff.py +237 -0
  272. opscloud/ui/widgets/effort_selector.py +166 -0
  273. opscloud/ui/widgets/goal_review.py +612 -0
  274. opscloud/ui/widgets/goal_status.py +57 -0
  275. opscloud/ui/widgets/infra_panel.py +141 -0
  276. opscloud/ui/widgets/install_confirm.py +114 -0
  277. opscloud/ui/widgets/loading.py +170 -0
  278. opscloud/ui/widgets/mcp_viewer.py +1204 -0
  279. opscloud/ui/widgets/messages.py +4461 -0
  280. opscloud/ui/widgets/model_selector.py +863 -0
  281. opscloud/ui/widgets/notification_center.py +108 -0
  282. opscloud/ui/widgets/notification_settings.py +119 -0
  283. opscloud/ui/widgets/operation_card.py +106 -0
  284. opscloud/ui/widgets/permissions_manager.py +667 -0
  285. opscloud/ui/widgets/plugin_manager.py +1212 -0
  286. opscloud/ui/widgets/pool_selector.py +376 -0
  287. opscloud/ui/widgets/preamble.py +53 -0
  288. opscloud/ui/widgets/skills_viewer.py +432 -0
  289. opscloud/ui/widgets/status.py +1037 -0
  290. opscloud/ui/widgets/subagent_panel.py +927 -0
  291. opscloud/ui/widgets/theme_selector.py +232 -0
  292. opscloud/ui/widgets/thread_selector.py +358 -0
  293. opscloud/ui/widgets/toast.py +31 -0
  294. opscloud/ui/widgets/tool_display.py +325 -0
  295. opscloud/ui/widgets/tool_renderers.py +234 -0
  296. opscloud/ui/widgets/tool_widgets.py +278 -0
  297. opscloud/ui/widgets/welcome.py +421 -0
  298. opscloud/ui/widgets/welcome_popup.py +102 -0
  299. opscloud/utils/__init__.py +24 -0
  300. opscloud/utils/cost_estimation.py +169 -0
  301. opscloud/utils/git.py +390 -0
  302. opscloud/utils/logger.py +571 -0
  303. opscloud/utils/session_stats.py +115 -0
  304. opscloud/utils/startup_error.py +37 -0
  305. talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
  306. talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
  307. talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
  308. talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
  309. talkops_opscloud-0.3.0b1.dist-info/licenses/LICENSE +175 -0
@@ -0,0 +1,651 @@
1
+ """Jev-powered System One tool-call safety classifier.
2
+
3
+ Evaluates semantic blast radius and mutation likelihood in <100ms using
4
+ TypeSafe AI Jev and LangChain-native decision primitives (Noul and Score).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from collections.abc import Mapping, Sequence
11
+ import os
12
+ from pathlib import Path
13
+ import re
14
+ import time
15
+ from typing import Any
16
+
17
+ from langchain_core.messages import ToolCall
18
+ from langchain_core.tools import BaseTool
19
+ from pydantic import BaseModel, Field
20
+
21
+ from opscloud.utils.logger import get_logger
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ class JevToolVerdict(BaseModel):
27
+ """Calibrated safety evaluation verdict produced by Jev System One."""
28
+
29
+ tool_call_id: str
30
+ tool_name: str
31
+ is_mutating: bool
32
+ mutating_probability: float
33
+ blast_radius: float
34
+ risk_level: int = Field(description="0=Safe, 1=Controlled/Reversible, 2=Critical")
35
+ requires_human_interrupt: bool
36
+ rationale: str
37
+ latency_ms: int = 0
38
+ action_type: str = "standard_tool"
39
+ caller_agent: str = "main"
40
+ intent_alignment_probability: float = 1.0
41
+ confidence: float = 1.0
42
+ critical_probability: float = 0.0
43
+ request_id: str | None = None
44
+ input_tokens: int | None = None
45
+ output_tokens: int | None = None
46
+ subagent_name: str | None = None
47
+ mcp_server: str | None = None
48
+
49
+
50
+ class JevSecurityClassifier:
51
+ """System One tool-call safety classifier powered by TypeSafe AI Jev."""
52
+
53
+ def __init__(
54
+ self,
55
+ api_key: str | None = None,
56
+ timeout_seconds: float = 2.0,
57
+ ) -> None:
58
+ """Initialize the Jev classifier with API key and timeout budget."""
59
+ from opscloud.config.settings import get_settings, resolve_env_var
60
+ if api_key is not None:
61
+ resolved_key = api_key if api_key else None
62
+ self._explicit_key = True
63
+ else:
64
+ settings_key = getattr(get_settings(), "typesafe_api_key", None)
65
+ resolved_key = settings_key or resolve_env_var("TYPESAFE_API_KEY", fallback_names=("JEV_API_KEY",))
66
+ self._explicit_key = False
67
+
68
+ self._api_key = resolved_key
69
+ self.timeout_seconds = timeout_seconds
70
+ self._classifier = None
71
+
72
+ def _get_classifier(self) -> Any:
73
+ """Lazily initialize TypeSafeClassifier instance."""
74
+ if self._classifier is None:
75
+ from langchain_typesafe import TypeSafeClassifier
76
+ if self._api_key:
77
+ self._classifier = TypeSafeClassifier(api_key=self._api_key)
78
+ else:
79
+ self._classifier = TypeSafeClassifier()
80
+ return self._classifier
81
+
82
+ def is_available(self) -> bool:
83
+ """Check whether TypeSafe API key or credentials are configured."""
84
+ if self._explicit_key:
85
+ return bool(self._api_key)
86
+ if self._api_key:
87
+ return True
88
+ from opscloud.config.settings import get_settings, resolve_env_var
89
+ settings_key = getattr(get_settings(), "typesafe_api_key", None)
90
+ if settings_key:
91
+ return True
92
+ return bool(resolve_env_var("TYPESAFE_API_KEY", fallback_names=("JEV_API_KEY",)))
93
+
94
+ def _compact_arguments(self, raw_args: dict[str, Any], max_len: int = 2000) -> dict[str, Any]:
95
+ """Truncate massive file contents or payloads to prevent Jev context bloat (32k ceiling)."""
96
+ compacted: dict[str, Any] = {}
97
+ for k, v in raw_args.items():
98
+ s = str(v)
99
+ if len(s) > max_len:
100
+ compacted[k] = s[:max_len] + f"... [truncated {len(s) - max_len} chars]"
101
+ else:
102
+ compacted[k] = v
103
+ return compacted
104
+
105
+ def _detect_action_type(
106
+ self,
107
+ tool_name: str,
108
+ tool_args: dict[str, Any],
109
+ tool: BaseTool | None = None,
110
+ ) -> str:
111
+ """Identify the operational category and provenance of the tool call."""
112
+ normalized = tool_name.lower()
113
+ if (
114
+ normalized in {"task", "subagent", "delegate_task", "call_subagent", "spawn_subagent"}
115
+ or "subagent_type" in tool_args
116
+ ):
117
+ return "subagent_dispatch"
118
+
119
+ if (
120
+ normalized.startswith("mcp__")
121
+ or ":" in normalized
122
+ or (
123
+ tool is not None
124
+ and hasattr(tool, "metadata")
125
+ and bool(
126
+ (tool.metadata or {}).get("_mcp_server")
127
+ or (tool.metadata or {}).get("is_mcp")
128
+ or (tool.metadata or {}).get("server_name")
129
+ )
130
+ )
131
+ ):
132
+ return "mcp_tool"
133
+
134
+ if normalized in {"execute", "execute_command", "run_command", "shell", "bash", "terminal", "sh"}:
135
+ return "shell_command"
136
+
137
+ if (
138
+ normalized in {"run_skill", "execute_skill", "run_skill_script"}
139
+ or any("skills/" in str(v) for v in tool_args.values())
140
+ ):
141
+ return "skill_execution"
142
+
143
+ if normalized in {
144
+ "write_file",
145
+ "edit_file",
146
+ "replace_file_content",
147
+ "multi_replace_file_content",
148
+ "create_temp_artifact",
149
+ "delete_temp_artifact",
150
+ }:
151
+ return "filesystem_operation"
152
+
153
+ return "standard_tool"
154
+
155
+ def _compact_tool_description(self, desc: str, max_chars: int = 250) -> str:
156
+ """Extract primary semantic purpose of the tool, omitting massive usage manuals."""
157
+ if not desc:
158
+ return ""
159
+ summary = desc.split("\n\n")[0].strip()
160
+ lines = summary.split("\n")
161
+ clean = lines[0].strip() if lines else summary
162
+ if len(clean) > max_chars:
163
+ return clean[:max_chars] + "..."
164
+ return clean
165
+
166
+ def _extract_shell_details(self, tool_args: dict[str, Any]) -> dict[str, Any]:
167
+ """Extract command string and pre-compute deterministic AST safety metrics."""
168
+ raw_cmd = str(
169
+ tool_args.get("command")
170
+ or tool_args.get("cmd")
171
+ or tool_args.get("CommandLine")
172
+ or ""
173
+ ).strip()
174
+
175
+ is_readonly = False
176
+ is_destructive = False
177
+ ast_tier = 3
178
+ ast_reason = "No command provided"
179
+ is_skill_script = "skills/" in raw_cmd or ".opscloud/skills" in raw_cmd
180
+
181
+ if raw_cmd:
182
+ try:
183
+ from opscloud.security.cli_ast_evaluator import evaluate_cli_safety
184
+
185
+ ast_eval = evaluate_cli_safety(raw_cmd)
186
+ is_readonly = bool(ast_eval.get("is_readonly", False))
187
+ is_destructive = bool(ast_eval.get("is_destructive", False))
188
+ ast_tier = int(ast_eval.get("tier", 3))
189
+ ast_reason = str(ast_eval.get("reason", ""))
190
+ except Exception as exc:
191
+ ast_reason = f"AST evaluation error: {exc}"
192
+
193
+ return {
194
+ "command": raw_cmd[:600],
195
+ "is_ast_readonly": is_readonly,
196
+ "is_ast_destructive": is_destructive,
197
+ "ast_tier": ast_tier,
198
+ "ast_reason": ast_reason,
199
+ "is_skill_script": is_skill_script,
200
+ }
201
+
202
+ def _extract_mcp_details(
203
+ self,
204
+ tool_name: str,
205
+ tool: BaseTool | None = None,
206
+ ) -> dict[str, Any]:
207
+ """Extract MCP server ownership, description, and spec annotations."""
208
+ server_name = ""
209
+ clean_action = tool_name
210
+ if tool_name.startswith("mcp__"):
211
+ parts = tool_name.split("__")
212
+ if len(parts) >= 2:
213
+ server_name = parts[1]
214
+ clean_action = "__".join(parts[2:]) if len(parts) > 2 else parts[1]
215
+ elif ":" in tool_name:
216
+ parts = tool_name.split(":", 1)
217
+ server_name = parts[0]
218
+ clean_action = parts[1]
219
+
220
+ tool_meta = dict(tool.metadata or {}) if tool is not None and hasattr(tool, "metadata") else {}
221
+ if not server_name and "_mcp_server" in tool_meta:
222
+ server_name = str(tool_meta["_mcp_server"])
223
+ if not server_name and "server_name" in tool_meta:
224
+ server_name = str(tool_meta["server_name"])
225
+
226
+ return {
227
+ "server_name": server_name or "external_mcp",
228
+ "mcp_action": clean_action,
229
+ "read_only_hint": bool(tool_meta.get("readOnlyHint", False)),
230
+ "destructive_hint": bool(tool_meta.get("destructiveHint", False)),
231
+ "idempotent_hint": bool(tool_meta.get("idempotentHint", False)),
232
+ "open_world_hint": bool(tool_meta.get("openWorldHint", False)),
233
+ }
234
+
235
+ def _extract_subagent_details(self, tool_args: dict[str, Any]) -> dict[str, Any]:
236
+ """Extract target subagent role, delegated prompt/instructions, and capability boundaries."""
237
+ target_subagent = str(
238
+ tool_args.get("subagent_type")
239
+ or tool_args.get("name")
240
+ or tool_args.get("agent")
241
+ or "subagent"
242
+ )
243
+ task_instruction = str(
244
+ tool_args.get("description")
245
+ or tool_args.get("prompt")
246
+ or tool_args.get("instruction")
247
+ or tool_args.get("task")
248
+ or ""
249
+ )
250
+
251
+ sub_desc = ""
252
+ allowed_tools: list[str] = []
253
+ allowed_skills: list[str] = []
254
+ has_exec_privilege = False
255
+ try:
256
+ from opscloud.subagents.loader import list_subagents
257
+
258
+ metas = list_subagents()
259
+ matched = next((m for m in metas if m.get("name") == target_subagent), None)
260
+ if matched:
261
+ sub_desc = matched.get("description", "")
262
+ allowed_tools = list(matched.get("tools") or [])
263
+ allowed_skills = list(matched.get("skills") or [])
264
+ if any(t in {"execute", "run_command", "write_file", "edit_file"} for t in allowed_tools):
265
+ has_exec_privilege = True
266
+ else:
267
+ # Unrestricted or general-purpose subagent
268
+ has_exec_privilege = True
269
+ except Exception:
270
+ has_exec_privilege = True
271
+
272
+ return {
273
+ "target_subagent": target_subagent,
274
+ "delegated_task": task_instruction[:600],
275
+ "subagent_description": sub_desc[:300],
276
+ "allowed_tools": allowed_tools,
277
+ "allowed_skills": allowed_skills,
278
+ "has_execution_privilege": has_exec_privilege,
279
+ }
280
+
281
+ async def evaluate_call(
282
+ self,
283
+ tool_call_id: str,
284
+ tool_name: str,
285
+ tool_args: dict[str, Any],
286
+ active_environment: str = "production",
287
+ worktree_root: str = ".",
288
+ *,
289
+ tool: BaseTool | None = None,
290
+ user_prompt: str = "",
291
+ user_goal: str = "",
292
+ caller_agent: str = "main",
293
+ is_deep_agent: bool = False,
294
+ active_skill: str | None = None,
295
+ ) -> JevToolVerdict:
296
+ """Evaluate a tool call across multi-agent, MCP, shell, and skill contexts."""
297
+ from langchain_typesafe import Noul, NoulCriteria, Score
298
+
299
+ action_type = self._detect_action_type(tool_name, tool_args, tool=tool)
300
+ if not self.is_available():
301
+ # Fail closed: missing credentials require human interrupt
302
+ return JevToolVerdict(
303
+ tool_call_id=tool_call_id,
304
+ tool_name=tool_name,
305
+ is_mutating=True,
306
+ mutating_probability=1.0,
307
+ blast_radius=2.0,
308
+ risk_level=2,
309
+ requires_human_interrupt=True,
310
+ rationale="TypeSafe API key (TYPESAFE_API_KEY) not configured; failing closed to human confirmation.",
311
+ action_type=action_type,
312
+ caller_agent=caller_agent,
313
+ )
314
+
315
+ compact_args = self._compact_arguments(tool_args)
316
+ is_prod = active_environment.lower() in {"prod", "production", "live"}
317
+
318
+ # 1. Action-specific context extraction
319
+ shell_details: dict[str, Any] | None = None
320
+ if action_type == "shell_command":
321
+ shell_details = self._extract_shell_details(tool_args)
322
+
323
+ mcp_details: dict[str, Any] | None = None
324
+ if action_type == "mcp_tool":
325
+ mcp_details = self._extract_mcp_details(tool_name, tool=tool)
326
+
327
+ subagent_details: dict[str, Any] | None = None
328
+ if action_type == "subagent_dispatch":
329
+ subagent_details = self._extract_subagent_details(tool_args)
330
+
331
+ skill_details: dict[str, Any] | None = None
332
+ if active_skill or action_type == "skill_execution" or (shell_details and shell_details.get("is_skill_script")):
333
+ skill_details = {
334
+ "active_skill": active_skill or "custom_skill",
335
+ "invoking_tool": tool_name,
336
+ "is_skill_script": bool(shell_details and shell_details.get("is_skill_script")),
337
+ }
338
+
339
+ # 2. Build multi-dimensional state payload for Jev
340
+ state_payload: dict[str, Any] = {
341
+ "action_type": action_type,
342
+ "tool_name": tool_name,
343
+ "tool_description": self._compact_tool_description(tool.description if tool and tool.description else ""),
344
+ "tool_arguments": compact_args,
345
+ "environment": active_environment,
346
+ "is_production": is_prod,
347
+ "worktree_root": worktree_root,
348
+ "caller_context": {
349
+ "agent_name": caller_agent,
350
+ "is_deep_agent": is_deep_agent,
351
+ "agent_role": "subagent" if is_deep_agent else "main_orchestrator",
352
+ },
353
+ }
354
+
355
+ if shell_details:
356
+ state_payload["shell_details"] = shell_details
357
+ if mcp_details:
358
+ state_payload["mcp_details"] = mcp_details
359
+ if subagent_details:
360
+ state_payload["subagent_details"] = subagent_details
361
+ if skill_details:
362
+ state_payload["skill_details"] = skill_details
363
+
364
+ user_intent: dict[str, str] = {}
365
+ if user_prompt:
366
+ user_intent["literal_user_request"] = user_prompt[:400]
367
+ if user_goal:
368
+ user_intent["active_goal"] = user_goal[:200]
369
+ if user_intent:
370
+ state_payload["user_intent"] = user_intent
371
+
372
+ # 3. Formulate calibrated questions
373
+ questions: dict[str, Any] = {
374
+ "is_mutating": Noul(
375
+ instructions=(
376
+ "Does this action (whether via external shell command, MCP tool, "
377
+ "subagent delegation, skill script, or filesystem operation) alter, "
378
+ "create, delete, or mutate infrastructure, cloud services, cluster state, "
379
+ "files, or persistent system data?"
380
+ ),
381
+ criteria=NoulCriteria(
382
+ true=(
383
+ "Modifies live resources, writes/edits files, executes state-changing CLI commands, "
384
+ "scales pods/instances, alters configs, or dispatches a subagent with mutating tasks."
385
+ ),
386
+ false=(
387
+ "Purely read-only inspection, search, status query, log retrieval, "
388
+ "dry-run validation, read_file, or subagent dispatch for read-only triage."
389
+ ),
390
+ ),
391
+ ),
392
+ "blast_radius": Score(
393
+ instructions=(
394
+ "Rate the operational blast radius and hazard of this action across "
395
+ "cloud infrastructure, Kubernetes clusters, external APIs, and local environment."
396
+ ),
397
+ criteria=[
398
+ "Negligible: Harmless read-only inspection, status query, local temporary check with zero side-effects.",
399
+ "Controlled: Reversible modification, non-production staging change, safe file edit within worktree, or scoped low-risk subagent execution.",
400
+ "Critical: Modifies or targets production resources, terminates instances, drops database/namespace, deletes deployments, alters credentials/RBAC, force pushes, executes untrusted external scripts, or dispatches unconstrained high-privilege subagents.",
401
+ ],
402
+ ),
403
+ }
404
+
405
+ if user_intent:
406
+ questions["intent_alignment"] = Noul(
407
+ instructions=(
408
+ "Is this action directly aligned with and authorized by the user's "
409
+ "literal request and stated goal?"
410
+ ),
411
+ criteria=NoulCriteria(
412
+ true=(
413
+ "The action directly fulfills, investigates, or progresses what the user "
414
+ "explicitly requested without unrequested collateral side effects."
415
+ ),
416
+ false=(
417
+ "The action performs unrequested modifications, targets unexpected infrastructure, "
418
+ "escalates permissions unnecessarily, or diverges from the user's prompt."
419
+ ),
420
+ ),
421
+ )
422
+
423
+ classifier = self._get_classifier()
424
+ start_time = time.monotonic()
425
+ run_config = {
426
+ "run_name": "jev_system_one_classifier",
427
+ "tags": ["opscloud:smart", "opscloud:jev"],
428
+ "metadata": {
429
+ "tool_name": tool_name,
430
+ "action_type": action_type,
431
+ "caller_agent": caller_agent,
432
+ "environment": active_environment,
433
+ },
434
+ }
435
+ try:
436
+ async with asyncio.timeout(self.timeout_seconds):
437
+ response = await classifier.ainvoke(
438
+ {
439
+ "state": state_payload,
440
+ "questions": questions,
441
+ },
442
+ config=run_config,
443
+ )
444
+ except TimeoutError:
445
+ latency_ms = int((time.monotonic() - start_time) * 1000)
446
+ logger.warning("Jev safety classifier timed out after %ss for %s", self.timeout_seconds, tool_name)
447
+ return JevToolVerdict(
448
+ tool_call_id=tool_call_id,
449
+ tool_name=tool_name,
450
+ is_mutating=True,
451
+ mutating_probability=1.0,
452
+ blast_radius=2.0,
453
+ risk_level=2,
454
+ requires_human_interrupt=True,
455
+ rationale=f"Jev classifier timed out ({self.timeout_seconds}s); failing closed to human approval.",
456
+ latency_ms=latency_ms,
457
+ action_type=action_type,
458
+ caller_agent=caller_agent,
459
+ )
460
+ except Exception as exc:
461
+ latency_ms = int((time.monotonic() - start_time) * 1000)
462
+ logger.warning("Jev safety classifier error for %s: %s", tool_name, exc)
463
+ return JevToolVerdict(
464
+ tool_call_id=tool_call_id,
465
+ tool_name=tool_name,
466
+ is_mutating=True,
467
+ mutating_probability=1.0,
468
+ blast_radius=2.0,
469
+ risk_level=2,
470
+ requires_human_interrupt=True,
471
+ rationale=f"Jev classifier error ({type(exc).__name__}); failing closed to human approval.",
472
+ latency_ms=latency_ms,
473
+ action_type=action_type,
474
+ caller_agent=caller_agent,
475
+ )
476
+
477
+ latency_ms = int((time.monotonic() - start_time) * 1000)
478
+ p_mutating = float(response.nouls["is_mutating"].noul)
479
+
480
+ blast_obj = response.scores.get("blast_radius") if hasattr(response, "scores") else None
481
+ score_val = float(getattr(blast_obj, "score", 0.0)) if blast_obj else 0.0
482
+
483
+ raw_conf = getattr(blast_obj, "confidence", None)
484
+ confidence = float(raw_conf) if isinstance(raw_conf, (int, float)) else 1.0
485
+
486
+ probs = getattr(blast_obj, "probabilities", None)
487
+ if isinstance(probs, dict):
488
+ raw_p = probs.get("2", 0.0) or probs.get(2, 0.0)
489
+ p_critical = float(raw_p) if isinstance(raw_p, (int, float)) else 0.0
490
+ else:
491
+ p_critical = 0.0
492
+
493
+ p_aligned = (
494
+ float(response.nouls["intent_alignment"].noul)
495
+ if "intent_alignment" in response.nouls
496
+ else 1.0
497
+ )
498
+
499
+ raw_req_id = getattr(response, "request_id", None)
500
+ req_id = raw_req_id if isinstance(raw_req_id, str) else None
501
+
502
+ usage = getattr(response, "usage", None)
503
+ input_tokens = None
504
+ output_tokens = None
505
+ if isinstance(usage, dict):
506
+ input_tokens = usage.get("input_tokens")
507
+ output_tokens = usage.get("output_tokens")
508
+ elif usage is not None and not hasattr(usage, "_mock_return_value"):
509
+ raw_in = getattr(usage, "input_tokens", None)
510
+ raw_out = getattr(usage, "output_tokens", None)
511
+ input_tokens = raw_in if isinstance(raw_in, int) else None
512
+ output_tokens = raw_out if isinstance(raw_out, int) else None
513
+
514
+ is_mutating = p_mutating >= 0.30
515
+ risk_level = round(score_val)
516
+
517
+ # Build descriptive action summary for user and audit log
518
+ subagent_name: str | None = None
519
+ mcp_server: str | None = None
520
+ if shell_details:
521
+ preview = shell_details["command"][:60]
522
+ action_summary = f"shell command '{preview}'"
523
+ elif mcp_details:
524
+ mcp_server = mcp_details.get("server_name")
525
+ action_summary = f"MCP tool '{tool_name}' on server '{mcp_server}'"
526
+ elif subagent_details:
527
+ subagent_name = subagent_details.get("target_subagent")
528
+ action_summary = f"dispatching subagent '{subagent_name}'"
529
+ elif skill_details:
530
+ action_summary = f"skill execution '{skill_details.get('active_skill')}'"
531
+ else:
532
+ action_summary = f"tool '{tool_name}'"
533
+
534
+ # 4. Multi-tier decision policy
535
+ requires_interrupt = False
536
+ if is_mutating:
537
+ if is_prod:
538
+ requires_interrupt = True
539
+ rationale = f"Mutating action targeting production environment ({action_summary})"
540
+ elif p_aligned < 0.60:
541
+ requires_interrupt = True
542
+ rationale = (
543
+ f"Mutating action diverges from user request/goal (alignment prob: {p_aligned:.2f}) on {action_summary}"
544
+ )
545
+ elif (
546
+ score_val >= 1.2
547
+ or p_critical >= 0.35
548
+ or (shell_details and shell_details.get("is_ast_destructive"))
549
+ or (mcp_details and mcp_details.get("destructive_hint"))
550
+ ):
551
+ requires_interrupt = True
552
+ if p_critical >= 0.35 and score_val < 1.2:
553
+ rationale = f"Elevated Critical blast radius probability ({p_critical:.0%}) on {action_summary}"
554
+ else:
555
+ rationale = f"Elevated blast radius ({score_val:.2f}) on {action_summary}"
556
+ elif confidence < 0.65:
557
+ requires_interrupt = True
558
+ rationale = f"Low classifier confidence ({confidence:.2f}); escalating to human confirmation for {action_summary}"
559
+ elif action_type == "subagent_dispatch" and (
560
+ score_val >= 1.0 or (subagent_details and subagent_details.get("has_execution_privilege") and p_mutating >= 0.50)
561
+ ):
562
+ requires_interrupt = True
563
+ rationale = f"Subagent delegation with mutating/execution capabilities ({action_summary})"
564
+ elif is_deep_agent and score_val >= 1.0:
565
+ requires_interrupt = True
566
+ rationale = f"Autonomous deep agent '{caller_agent}' attempting state modification ({action_summary})"
567
+ else:
568
+ rationale = (
569
+ f"Autonomous execution permitted in {active_environment} "
570
+ f"(risk: {score_val:.2f}, prob: {p_mutating:.2f}) for {action_summary}"
571
+ )
572
+ elif confidence < 0.50 and score_val >= 0.8:
573
+ requires_interrupt = True
574
+ rationale = f"Ambiguous risk profile with low confidence ({confidence:.2f}) for {action_summary}"
575
+ else:
576
+ rationale = f"Safe non-mutating operation (risk: {score_val:.2f}, prob: {p_mutating:.2f}) for {action_summary}"
577
+
578
+ logger.info(
579
+ "Jev verdict for %s [%s] (req_id=%s in_tok=%s): mutating=%s (p=%.2f) blast_radius=%.2f (p_crit=%.2f, conf=%.2f) risk=%d interrupt=%s latency=%dms rationale=%s",
580
+ tool_name,
581
+ action_type,
582
+ req_id,
583
+ input_tokens,
584
+ is_mutating,
585
+ p_mutating,
586
+ score_val,
587
+ p_critical,
588
+ confidence,
589
+ risk_level,
590
+ requires_interrupt,
591
+ latency_ms,
592
+ rationale,
593
+ )
594
+
595
+ return JevToolVerdict(
596
+ tool_call_id=tool_call_id,
597
+ tool_name=tool_name,
598
+ is_mutating=is_mutating,
599
+ mutating_probability=p_mutating,
600
+ blast_radius=score_val,
601
+ risk_level=risk_level,
602
+ requires_human_interrupt=requires_interrupt,
603
+ rationale=rationale,
604
+ latency_ms=latency_ms,
605
+ action_type=action_type,
606
+ caller_agent=caller_agent,
607
+ intent_alignment_probability=p_aligned,
608
+ confidence=confidence,
609
+ critical_probability=p_critical,
610
+ request_id=req_id if req_id else None,
611
+ input_tokens=int(input_tokens) if input_tokens is not None else None,
612
+ output_tokens=int(output_tokens) if output_tokens is not None else None,
613
+ subagent_name=subagent_name,
614
+ mcp_server=mcp_server,
615
+ )
616
+
617
+ async def evaluate_batch(
618
+ self,
619
+ calls: Sequence[ToolCall],
620
+ tools: Mapping[str, BaseTool] | None = None,
621
+ active_environment: str = "production",
622
+ worktree_root: str = ".",
623
+ *,
624
+ user_prompt: str = "",
625
+ user_goal: str = "",
626
+ caller_agent: str = "main",
627
+ is_deep_agent: bool = False,
628
+ active_skill: str | None = None,
629
+ ) -> list[JevToolVerdict]:
630
+ """Evaluate a batch of tool calls concurrently in parallel with rich context."""
631
+ tools_map = tools or {}
632
+ tasks = [
633
+ self.evaluate_call(
634
+ tool_call_id=str(call.get("id", "")),
635
+ tool_name=call.get("name", ""),
636
+ tool_args=dict(call.get("args") or {}),
637
+ active_environment=active_environment,
638
+ worktree_root=worktree_root,
639
+ tool=tools_map.get(call.get("name", "")),
640
+ user_prompt=user_prompt,
641
+ user_goal=user_goal,
642
+ caller_agent=caller_agent,
643
+ is_deep_agent=is_deep_agent,
644
+ active_skill=active_skill,
645
+ )
646
+ for call in calls
647
+ ]
648
+ return await asyncio.gather(*tasks)
649
+
650
+
651
+ __all__ = ["JevSecurityClassifier", "JevToolVerdict"]