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.
- opscloud/__init__.py +5 -0
- opscloud/__main__.py +6 -0
- opscloud/_constants.py +53 -0
- opscloud/_debug.py +145 -0
- opscloud/_debug_buffer.py +250 -0
- opscloud/_textual_patches.py +367 -0
- opscloud/_version.py +15 -0
- opscloud/agent/__init__.py +27 -0
- opscloud/agent/config.py +168 -0
- opscloud/agent/factory.py +1103 -0
- opscloud/approval_mode.py +44 -0
- opscloud/backend/__init__.py +49 -0
- opscloud/backend/composite.py +89 -0
- opscloud/backend/local.py +183 -0
- opscloud/backend/registry.py +79 -0
- opscloud/backend/sandbox_config.py +107 -0
- opscloud/backend/sandbox_factory.py +432 -0
- opscloud/built_in_skills/remember/SKILL.md +124 -0
- opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
- opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
- opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
- opscloud/cli/__init__.py +1 -0
- opscloud/cli/commands/__init__.py +15 -0
- opscloud/cli/commands/auth.py +45 -0
- opscloud/cli/commands/doctor.py +100 -0
- opscloud/cli/commands/mcp.py +137 -0
- opscloud/cli/commands/threads.py +51 -0
- opscloud/cli/interactive.py +154 -0
- opscloud/cli/main.py +903 -0
- opscloud/cli/non_interactive.py +425 -0
- opscloud/cli/server_manager.py +58 -0
- opscloud/commands/__init__.py +19 -0
- opscloud/commands/_base.py +110 -0
- opscloud/commands/_guards.py +52 -0
- opscloud/commands/_router.py +146 -0
- opscloud/commands/_types.py +36 -0
- opscloud/commands/core/__init__.py +47 -0
- opscloud/commands/core/auth.py +94 -0
- opscloud/commands/core/bug.py +46 -0
- opscloud/commands/core/clear.py +106 -0
- opscloud/commands/core/cloud.py +185 -0
- opscloud/commands/core/compact.py +128 -0
- opscloud/commands/core/config_cmd.py +120 -0
- opscloud/commands/core/context.py +220 -0
- opscloud/commands/core/cost.py +44 -0
- opscloud/commands/core/doctor.py +535 -0
- opscloud/commands/core/effort.py +108 -0
- opscloud/commands/core/exit_cmd.py +41 -0
- opscloud/commands/core/fast.py +148 -0
- opscloud/commands/core/help_cmd.py +94 -0
- opscloud/commands/core/mcp.py +134 -0
- opscloud/commands/core/model.py +109 -0
- opscloud/commands/core/permissions.py +196 -0
- opscloud/commands/core/plugins.py +92 -0
- opscloud/commands/core/pool.py +108 -0
- opscloud/commands/core/resume.py +97 -0
- opscloud/commands/core/skills.py +178 -0
- opscloud/commands/power/__init__.py +51 -0
- opscloud/commands/power/agents.py +98 -0
- opscloud/commands/power/btw.py +85 -0
- opscloud/commands/power/copy.py +97 -0
- opscloud/commands/power/goal.py +548 -0
- opscloud/commands/power/loop.py +295 -0
- opscloud/commands/power/memory.py +287 -0
- opscloud/commands/power/review.py +152 -0
- opscloud/commands/power/rubric.py +287 -0
- opscloud/commands/power/runtime.py +321 -0
- opscloud/commands/power/skill_creator.py +70 -0
- opscloud/commands/power/skill_invoke.py +165 -0
- opscloud/commands/power/tasks.py +209 -0
- opscloud/commands/power/trace.py +117 -0
- opscloud/commands/power/ui_toggles.py +135 -0
- opscloud/commands/power/version.py +52 -0
- opscloud/commands/registry.py +176 -0
- opscloud/config/__init__.py +114 -0
- opscloud/config/adapters/__init__.py +5 -0
- opscloud/config/adapters/postgres.py +776 -0
- opscloud/config/adapters/sqlite.py +769 -0
- opscloud/config/aws.py +248 -0
- opscloud/config/cloud_profiles.py +437 -0
- opscloud/config/env_vars.py +81 -0
- opscloud/config/langsmith.py +319 -0
- opscloud/config/manifest.py +817 -0
- opscloud/config/metadata.py +205 -0
- opscloud/config/paths.py +604 -0
- opscloud/config/plugins.py +130 -0
- opscloud/config/settings.py +834 -0
- opscloud/config/store.py +453 -0
- opscloud/config/store_factory.py +97 -0
- opscloud/config/toml_config.py +562 -0
- opscloud/editor.py +173 -0
- opscloud/exceptions.py +65 -0
- opscloud/file_ops.py +217 -0
- opscloud/hooks/__init__.py +139 -0
- opscloud/hooks/env.py +44 -0
- opscloud/hooks/interrupt.py +121 -0
- opscloud/hooks/models/__init__.py +109 -0
- opscloud/hooks/models/adapters.py +19 -0
- opscloud/hooks/models/config.py +79 -0
- opscloud/hooks/models/domain.py +434 -0
- opscloud/hooks/models/transport.py +37 -0
- opscloud/hooks/tools.py +231 -0
- opscloud/input.py +48 -0
- opscloud/integrations/__init__.py +80 -0
- opscloud/integrations/base.py +153 -0
- opscloud/integrations/content_filter.py +119 -0
- opscloud/integrations/event_bus.py +480 -0
- opscloud/integrations/hooks.py +238 -0
- opscloud/integrations/identity.py +181 -0
- opscloud/integrations/notifications.py +134 -0
- opscloud/integrations/stream_bridge.py +397 -0
- opscloud/integrations/thread_store.py +81 -0
- opscloud/json_types.py +20 -0
- opscloud/mcp/__init__.py +57 -0
- opscloud/mcp/config.py +203 -0
- opscloud/mcp/discovery.py +229 -0
- opscloud/mcp/mcp_info.py +109 -0
- opscloud/mcp/middleware.py +102 -0
- opscloud/mcp/preload.py +505 -0
- opscloud/mcp/raw_config.py +149 -0
- opscloud/mcp/semantic_profiler.py +305 -0
- opscloud/mcp/session_manager.py +727 -0
- opscloud/mcp/trust.py +105 -0
- opscloud/memory/__init__.py +27 -0
- opscloud/memory/branch.py +61 -0
- opscloud/memory/guard.py +19 -0
- opscloud/memory/onboarding.py +120 -0
- opscloud/memory/registry.py +134 -0
- opscloud/memory/store.py +223 -0
- opscloud/middleware/__init__.py +106 -0
- opscloud/middleware/_repository_bounds.py +459 -0
- opscloud/middleware/ask_user.py +152 -0
- opscloud/middleware/auto_mode.py +60 -0
- opscloud/middleware/auto_mode_hitl.py +2434 -0
- opscloud/middleware/compaction.py +566 -0
- opscloud/middleware/configurable_model.py +233 -0
- opscloud/middleware/cost_tracking.py +1021 -0
- opscloud/middleware/glm_stall_recovery.py +109 -0
- opscloud/middleware/goal_criteria.py +1234 -0
- opscloud/middleware/goal_state_notice.py +712 -0
- opscloud/middleware/goal_tools.py +407 -0
- opscloud/middleware/headless_mcp_guard.py +88 -0
- opscloud/middleware/jev_model_router.py +1018 -0
- opscloud/middleware/local_context.py +460 -0
- opscloud/middleware/mcp_context.py +151 -0
- opscloud/middleware/mcp_middleware.py +146 -0
- opscloud/middleware/memory_guard.py +393 -0
- opscloud/middleware/model_retry.py +205 -0
- opscloud/middleware/registry.py +137 -0
- opscloud/middleware/reliable_rubric.py +709 -0
- opscloud/middleware/resume_state.py +91 -0
- opscloud/middleware/server_hooks.py +1189 -0
- opscloud/middleware/shell_allow_list.py +90 -0
- opscloud/middleware/skills.py +382 -0
- opscloud/middleware/subagent_artifacts.py +84 -0
- opscloud/middleware/subagent_telemetry.py +261 -0
- opscloud/middleware/subagents.py +243 -0
- opscloud/middleware/tool_filter.py +384 -0
- opscloud/middleware/unified_system_message.py +62 -0
- opscloud/model/__init__.py +37 -0
- opscloud/model/config.py +2992 -0
- opscloud/model/factory.py +477 -0
- opscloud/model/pool.py +459 -0
- opscloud/model/reasoning.py +282 -0
- opscloud/offload.py +173 -0
- opscloud/output.py +64 -0
- opscloud/plugins/__init__.py +189 -0
- opscloud/plugins/_json.py +22 -0
- opscloud/plugins/adapters/__init__.py +13 -0
- opscloud/plugins/adapters/agents.py +112 -0
- opscloud/plugins/adapters/commands.py +190 -0
- opscloud/plugins/adapters/mcp.py +363 -0
- opscloud/plugins/adapters/skills.py +132 -0
- opscloud/plugins/commands_cli.py +541 -0
- opscloud/plugins/discovery.py +512 -0
- opscloud/plugins/manifest.py +335 -0
- opscloud/plugins/marketplace.py +893 -0
- opscloud/plugins/models.py +334 -0
- opscloud/plugins/project_plugins.py +317 -0
- opscloud/plugins/protocol.py +31 -0
- opscloud/plugins/store.py +905 -0
- opscloud/plugins/substitution.py +122 -0
- opscloud/project_utils.py +145 -0
- opscloud/prompts/__init__.py +37 -0
- opscloud/prompts/system.py +483 -0
- opscloud/prompts/templates/system_prompt.md +166 -0
- opscloud/prompts/types.py +54 -0
- opscloud/rubrics/__init__.py +42 -0
- opscloud/rubrics/evaluator.py +404 -0
- opscloud/rubrics/evidence_extractor.py +229 -0
- opscloud/rubrics/generator.py +239 -0
- opscloud/rubrics/jev_compiler.py +175 -0
- opscloud/rubrics/jev_grader.py +297 -0
- opscloud/rubrics/middleware.py +8 -0
- opscloud/schema/__init__.py +107 -0
- opscloud/schema/interrupts.py +981 -0
- opscloud/security/__init__.py +91 -0
- opscloud/security/approval_mode.py +528 -0
- opscloud/security/approval_mode_source.py +230 -0
- opscloud/security/cli_ast_evaluator.py +552 -0
- opscloud/security/jev_classifier.py +651 -0
- opscloud/security/shell_safety.py +340 -0
- opscloud/security/unicode_security.py +439 -0
- opscloud/security/url_validation.py +160 -0
- opscloud/server/__init__.py +13 -0
- opscloud/server/_server_config.py +158 -0
- opscloud/server/server.py +242 -0
- opscloud/server/server_graph.py +93 -0
- opscloud/skills/__init__.py +91 -0
- opscloud/skills/commands.py +1099 -0
- opscloud/skills/invocation.py +200 -0
- opscloud/skills/load.py +388 -0
- opscloud/skills/loader.py +23 -0
- opscloud/skills/merge.py +65 -0
- opscloud/skills/registry.py +263 -0
- opscloud/skills/sources.py +174 -0
- opscloud/skills/trust.py +417 -0
- opscloud/state/__init__.py +142 -0
- opscloud/state/base.py +154 -0
- opscloud/state/cloud_context.py +262 -0
- opscloud/state/goal_channels.py +124 -0
- opscloud/state/goal_state_limits.py +175 -0
- opscloud/state/resume_state.py +58 -0
- opscloud/state/service_context.py +281 -0
- opscloud/state/session.py +957 -0
- opscloud/state/state_migration.py +86 -0
- opscloud/subagents/__init__.py +30 -0
- opscloud/subagents/loader.py +331 -0
- opscloud/subagents/subagents_parser.py +117 -0
- opscloud/subagents/types.py +25 -0
- opscloud/tools/__init__.py +36 -0
- opscloud/tools/catalog.py +37 -0
- opscloud/tools/display.py +24 -0
- opscloud/tools/fetch_url.py +176 -0
- opscloud/tools/goal_tools.py +423 -0
- opscloud/tools/registry.py +105 -0
- opscloud/tools/thread.py +32 -0
- opscloud/tools/web_search.py +215 -0
- opscloud/ui/__init__.py +119 -0
- opscloud/ui/app.py +5400 -0
- opscloud/ui/app.tcss +479 -0
- opscloud/ui/clipboard.py +147 -0
- opscloud/ui/command_registry.py +482 -0
- opscloud/ui/message_store.py +152 -0
- opscloud/ui/permission_store.py +360 -0
- opscloud/ui/preferences.py +113 -0
- opscloud/ui/prompt_manager.py +96 -0
- opscloud/ui/remote_client.py +505 -0
- opscloud/ui/textual_adapter.py +1269 -0
- opscloud/ui/theme.py +728 -0
- opscloud/ui/ui_help.py +415 -0
- opscloud/ui/widgets/__init__.py +383 -0
- opscloud/ui/widgets/_ask_user_types.py +81 -0
- opscloud/ui/widgets/_copy_spans.py +52 -0
- opscloud/ui/widgets/_inline_prompt.py +290 -0
- opscloud/ui/widgets/_js_eval_display.py +139 -0
- opscloud/ui/widgets/_links.py +175 -0
- opscloud/ui/widgets/_tool_stream.py +308 -0
- opscloud/ui/widgets/agent_selector.py +297 -0
- opscloud/ui/widgets/approval.py +741 -0
- opscloud/ui/widgets/ask_user.py +539 -0
- opscloud/ui/widgets/auth.py +695 -0
- opscloud/ui/widgets/auth_manager.py +214 -0
- opscloud/ui/widgets/auto_mode_notice.py +146 -0
- opscloud/ui/widgets/autocomplete.py +709 -0
- opscloud/ui/widgets/chat_input.py +805 -0
- opscloud/ui/widgets/cloud_selector.py +528 -0
- opscloud/ui/widgets/config_manager.py +652 -0
- opscloud/ui/widgets/debug_console.py +1269 -0
- opscloud/ui/widgets/devops_renderers.py +184 -0
- opscloud/ui/widgets/diff.py +237 -0
- opscloud/ui/widgets/effort_selector.py +166 -0
- opscloud/ui/widgets/goal_review.py +612 -0
- opscloud/ui/widgets/goal_status.py +57 -0
- opscloud/ui/widgets/infra_panel.py +141 -0
- opscloud/ui/widgets/install_confirm.py +114 -0
- opscloud/ui/widgets/loading.py +170 -0
- opscloud/ui/widgets/mcp_viewer.py +1204 -0
- opscloud/ui/widgets/messages.py +4461 -0
- opscloud/ui/widgets/model_selector.py +863 -0
- opscloud/ui/widgets/notification_center.py +108 -0
- opscloud/ui/widgets/notification_settings.py +119 -0
- opscloud/ui/widgets/operation_card.py +106 -0
- opscloud/ui/widgets/permissions_manager.py +667 -0
- opscloud/ui/widgets/plugin_manager.py +1212 -0
- opscloud/ui/widgets/pool_selector.py +376 -0
- opscloud/ui/widgets/preamble.py +53 -0
- opscloud/ui/widgets/skills_viewer.py +432 -0
- opscloud/ui/widgets/status.py +1037 -0
- opscloud/ui/widgets/subagent_panel.py +927 -0
- opscloud/ui/widgets/theme_selector.py +232 -0
- opscloud/ui/widgets/thread_selector.py +358 -0
- opscloud/ui/widgets/toast.py +31 -0
- opscloud/ui/widgets/tool_display.py +325 -0
- opscloud/ui/widgets/tool_renderers.py +234 -0
- opscloud/ui/widgets/tool_widgets.py +278 -0
- opscloud/ui/widgets/welcome.py +421 -0
- opscloud/ui/widgets/welcome_popup.py +102 -0
- opscloud/utils/__init__.py +24 -0
- opscloud/utils/cost_estimation.py +169 -0
- opscloud/utils/git.py +390 -0
- opscloud/utils/logger.py +571 -0
- opscloud/utils/session_stats.py +115 -0
- opscloud/utils/startup_error.py +37 -0
- talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
- talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
- talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
- talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
- 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"]
|