agent_os_kernel 3.1.0__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.
- agent_control_plane/__init__.py +662 -0
- agent_control_plane/a2a_adapter.py +543 -0
- agent_control_plane/adapter.py +417 -0
- agent_control_plane/agent_hibernation.py +394 -0
- agent_control_plane/agent_kernel.py +470 -0
- agent_control_plane/compliance.py +720 -0
- agent_control_plane/constraint_graphs.py +478 -0
- agent_control_plane/control_plane.py +854 -0
- agent_control_plane/example_executors.py +195 -0
- agent_control_plane/execution_engine.py +231 -0
- agent_control_plane/flight_recorder.py +846 -0
- agent_control_plane/governance_layer.py +435 -0
- agent_control_plane/hf_utils.py +563 -0
- agent_control_plane/interfaces/__init__.py +55 -0
- agent_control_plane/interfaces/kernel_interface.py +361 -0
- agent_control_plane/interfaces/plugin_interface.py +497 -0
- agent_control_plane/interfaces/protocol_interfaces.py +387 -0
- agent_control_plane/kernel_space.py +1009 -0
- agent_control_plane/langchain_adapter.py +424 -0
- agent_control_plane/lifecycle.py +3113 -0
- agent_control_plane/mcp_adapter.py +653 -0
- agent_control_plane/ml_safety.py +563 -0
- agent_control_plane/multimodal.py +727 -0
- agent_control_plane/mute_agent.py +422 -0
- agent_control_plane/observability.py +787 -0
- agent_control_plane/orchestrator.py +482 -0
- agent_control_plane/plugin_registry.py +750 -0
- agent_control_plane/policy_engine.py +954 -0
- agent_control_plane/process_isolation.py +777 -0
- agent_control_plane/shadow_mode.py +310 -0
- agent_control_plane/signals.py +493 -0
- agent_control_plane/supervisor_agents.py +430 -0
- agent_control_plane/time_travel_debugger.py +557 -0
- agent_control_plane/tool_registry.py +452 -0
- agent_control_plane/vfs.py +697 -0
- agent_kernel/__init__.py +69 -0
- agent_kernel/analyzer.py +435 -0
- agent_kernel/auditor.py +36 -0
- agent_kernel/completeness_auditor.py +237 -0
- agent_kernel/detector.py +203 -0
- agent_kernel/kernel.py +744 -0
- agent_kernel/memory_manager.py +85 -0
- agent_kernel/models.py +374 -0
- agent_kernel/nudge_mechanism.py +263 -0
- agent_kernel/outcome_analyzer.py +338 -0
- agent_kernel/patcher.py +582 -0
- agent_kernel/semantic_analyzer.py +316 -0
- agent_kernel/semantic_purge.py +349 -0
- agent_kernel/simulator.py +449 -0
- agent_kernel/teacher.py +85 -0
- agent_kernel/triage.py +152 -0
- agent_os/__init__.py +409 -0
- agent_os/_adversarial_impl.py +200 -0
- agent_os/_circuit_breaker_impl.py +232 -0
- agent_os/_mcp_metrics.py +193 -0
- agent_os/adversarial.py +20 -0
- agent_os/agents_compat.py +490 -0
- agent_os/audit_logger.py +135 -0
- agent_os/base_agent.py +651 -0
- agent_os/circuit_breaker.py +34 -0
- agent_os/cli/__init__.py +659 -0
- agent_os/cli/cmd_audit.py +128 -0
- agent_os/cli/cmd_init.py +152 -0
- agent_os/cli/cmd_policy.py +41 -0
- agent_os/cli/cmd_policy_gen.py +180 -0
- agent_os/cli/cmd_validate.py +258 -0
- agent_os/cli/mcp_scan.py +265 -0
- agent_os/cli/output.py +192 -0
- agent_os/cli/policy_checker.py +330 -0
- agent_os/compat.py +74 -0
- agent_os/constraint_graph.py +234 -0
- agent_os/content_governance.py +140 -0
- agent_os/context_budget.py +305 -0
- agent_os/credential_redactor.py +224 -0
- agent_os/diff_policy.py +89 -0
- agent_os/egress_policy.py +159 -0
- agent_os/escalation.py +276 -0
- agent_os/event_bus.py +124 -0
- agent_os/exceptions.py +180 -0
- agent_os/execution_context_policy.py +141 -0
- agent_os/github_enterprise.py +96 -0
- agent_os/health.py +20 -0
- agent_os/integrations/__init__.py +279 -0
- agent_os/integrations/a2a_adapter.py +279 -0
- agent_os/integrations/agent_lightning/__init__.py +30 -0
- agent_os/integrations/anthropic_adapter.py +420 -0
- agent_os/integrations/autogen_adapter.py +620 -0
- agent_os/integrations/base.py +1137 -0
- agent_os/integrations/compat.py +229 -0
- agent_os/integrations/config.py +98 -0
- agent_os/integrations/conversation_guardian.py +957 -0
- agent_os/integrations/crewai_adapter.py +467 -0
- agent_os/integrations/drift_detector.py +425 -0
- agent_os/integrations/dry_run.py +124 -0
- agent_os/integrations/escalation.py +582 -0
- agent_os/integrations/gemini_adapter.py +364 -0
- agent_os/integrations/google_adk_adapter.py +633 -0
- agent_os/integrations/guardrails_adapter.py +394 -0
- agent_os/integrations/health.py +197 -0
- agent_os/integrations/langchain_adapter.py +654 -0
- agent_os/integrations/llamafirewall.py +343 -0
- agent_os/integrations/llamaindex_adapter.py +188 -0
- agent_os/integrations/logging.py +191 -0
- agent_os/integrations/maf_adapter.py +631 -0
- agent_os/integrations/mistral_adapter.py +365 -0
- agent_os/integrations/openai_adapter.py +816 -0
- agent_os/integrations/openai_agents_sdk.py +406 -0
- agent_os/integrations/policy_compose.py +171 -0
- agent_os/integrations/profiling.py +144 -0
- agent_os/integrations/pydantic_ai_adapter.py +420 -0
- agent_os/integrations/rate_limiter.py +130 -0
- agent_os/integrations/rbac.py +143 -0
- agent_os/integrations/registry.py +113 -0
- agent_os/integrations/scope_guard.py +303 -0
- agent_os/integrations/semantic_kernel_adapter.py +769 -0
- agent_os/integrations/smolagents_adapter.py +629 -0
- agent_os/integrations/templates.py +178 -0
- agent_os/integrations/token_budget.py +134 -0
- agent_os/integrations/tool_aliases.py +190 -0
- agent_os/integrations/webhooks.py +177 -0
- agent_os/lite.py +208 -0
- agent_os/mcp_gateway.py +385 -0
- agent_os/mcp_message_signer.py +273 -0
- agent_os/mcp_protocols.py +161 -0
- agent_os/mcp_response_scanner.py +232 -0
- agent_os/mcp_security.py +924 -0
- agent_os/mcp_session_auth.py +231 -0
- agent_os/mcp_sliding_rate_limiter.py +184 -0
- agent_os/memory_guard.py +409 -0
- agent_os/metrics.py +134 -0
- agent_os/mute.py +428 -0
- agent_os/mute_agent.py +209 -0
- agent_os/policies/__init__.py +77 -0
- agent_os/policies/async_evaluator.py +275 -0
- agent_os/policies/backends.py +670 -0
- agent_os/policies/bridge.py +169 -0
- agent_os/policies/budget.py +85 -0
- agent_os/policies/cli.py +294 -0
- agent_os/policies/conflict_resolution.py +270 -0
- agent_os/policies/data_classification.py +252 -0
- agent_os/policies/evaluator.py +239 -0
- agent_os/policies/policy_schema.json +228 -0
- agent_os/policies/rate_limiting.py +145 -0
- agent_os/policies/schema.py +115 -0
- agent_os/policies/shared.py +331 -0
- agent_os/prompt_injection.py +694 -0
- agent_os/providers.py +182 -0
- agent_os/py.typed +0 -0
- agent_os/retry.py +81 -0
- agent_os/reversibility.py +251 -0
- agent_os/sandbox.py +432 -0
- agent_os/sandbox_provider.py +140 -0
- agent_os/secure_codegen.py +525 -0
- agent_os/security_skills.py +538 -0
- agent_os/semantic_policy.py +422 -0
- agent_os/server/__init__.py +15 -0
- agent_os/server/__main__.py +25 -0
- agent_os/server/app.py +277 -0
- agent_os/server/models.py +104 -0
- agent_os/shift_left_metrics.py +130 -0
- agent_os/stateless.py +742 -0
- agent_os/supervisor.py +148 -0
- agent_os/task_outcome.py +148 -0
- agent_os/transparency.py +181 -0
- agent_os/trust_root.py +128 -0
- agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
- agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
- agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
- agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
- agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
- agent_os_observability/__init__.py +27 -0
- agent_os_observability/dashboards.py +898 -0
- agent_os_observability/metrics.py +398 -0
- agent_os_observability/server.py +223 -0
- agent_os_observability/tracer.py +232 -0
- agent_primitives/__init__.py +24 -0
- agent_primitives/failures.py +84 -0
- agent_primitives/py.typed +0 -0
- amb_core/__init__.py +177 -0
- amb_core/adapters/__init__.py +57 -0
- amb_core/adapters/aws_sqs_broker.py +376 -0
- amb_core/adapters/azure_servicebus_broker.py +340 -0
- amb_core/adapters/kafka_broker.py +260 -0
- amb_core/adapters/nats_broker.py +285 -0
- amb_core/adapters/rabbitmq_broker.py +235 -0
- amb_core/adapters/redis_broker.py +262 -0
- amb_core/broker.py +145 -0
- amb_core/bus.py +481 -0
- amb_core/cloudevents.py +509 -0
- amb_core/dlq.py +345 -0
- amb_core/hf_utils.py +536 -0
- amb_core/memory_broker.py +410 -0
- amb_core/models.py +141 -0
- amb_core/persistence.py +529 -0
- amb_core/schema.py +294 -0
- amb_core/tracing.py +358 -0
- atr/__init__.py +640 -0
- atr/access.py +348 -0
- atr/composition.py +645 -0
- atr/decorator.py +357 -0
- atr/executor.py +384 -0
- atr/health.py +557 -0
- atr/hf_utils.py +449 -0
- atr/injection.py +422 -0
- atr/metrics.py +440 -0
- atr/policies.py +403 -0
- atr/py.typed +2 -0
- atr/registry.py +452 -0
- atr/schema.py +480 -0
- atr/tools/safe/__init__.py +75 -0
- atr/tools/safe/calculator.py +467 -0
- atr/tools/safe/datetime_tool.py +443 -0
- atr/tools/safe/file_reader.py +402 -0
- atr/tools/safe/http_client.py +316 -0
- atr/tools/safe/json_parser.py +374 -0
- atr/tools/safe/text_tool.py +537 -0
- atr/tools/safe/toolkit.py +175 -0
- caas/__init__.py +162 -0
- caas/api/__init__.py +7 -0
- caas/api/server.py +1328 -0
- caas/caching.py +834 -0
- caas/cli.py +210 -0
- caas/conversation.py +223 -0
- caas/decay.py +72 -0
- caas/detection/__init__.py +9 -0
- caas/detection/detector.py +238 -0
- caas/enrichment.py +130 -0
- caas/gateway/__init__.py +27 -0
- caas/gateway/trust_gateway.py +474 -0
- caas/hf_utils.py +479 -0
- caas/ingestion/__init__.py +23 -0
- caas/ingestion/processors.py +253 -0
- caas/ingestion/structure_parser.py +188 -0
- caas/models.py +356 -0
- caas/pragmatic_truth.py +444 -0
- caas/routing/__init__.py +10 -0
- caas/routing/heuristic_router.py +58 -0
- caas/storage/__init__.py +9 -0
- caas/storage/store.py +389 -0
- caas/triad.py +213 -0
- caas/tuning/__init__.py +9 -0
- caas/tuning/tuner.py +329 -0
- caas/vfs/__init__.py +14 -0
- caas/vfs/filesystem.py +452 -0
- cmvk/__init__.py +218 -0
- cmvk/audit.py +402 -0
- cmvk/benchmarks.py +478 -0
- cmvk/constitutional.py +904 -0
- cmvk/hf_utils.py +301 -0
- cmvk/metrics.py +473 -0
- cmvk/profiles.py +300 -0
- cmvk/py.typed +0 -0
- cmvk/types.py +12 -0
- cmvk/verification.py +956 -0
- emk/__init__.py +89 -0
- emk/causal.py +352 -0
- emk/hf_utils.py +421 -0
- emk/indexer.py +83 -0
- emk/py.typed +0 -0
- emk/schema.py +204 -0
- emk/sleep_cycle.py +347 -0
- emk/store.py +281 -0
- iatp/__init__.py +166 -0
- iatp/attestation.py +461 -0
- iatp/cli.py +317 -0
- iatp/hf_utils.py +472 -0
- iatp/ipc_pipes.py +580 -0
- iatp/main.py +412 -0
- iatp/models/__init__.py +447 -0
- iatp/policy_engine.py +337 -0
- iatp/py.typed +2 -0
- iatp/recovery.py +321 -0
- iatp/security/__init__.py +270 -0
- iatp/sidecar/__init__.py +519 -0
- iatp/telemetry/__init__.py +164 -0
- iatp/tests/__init__.py +1 -0
- iatp/tests/test_attestation.py +370 -0
- iatp/tests/test_cli.py +131 -0
- iatp/tests/test_ed25519_attestation.py +211 -0
- iatp/tests/test_models.py +130 -0
- iatp/tests/test_policy_engine.py +347 -0
- iatp/tests/test_recovery.py +281 -0
- iatp/tests/test_security.py +222 -0
- iatp/tests/test_sidecar.py +167 -0
- iatp/tests/test_telemetry.py +175 -0
- mcp_kernel_server/__init__.py +28 -0
- mcp_kernel_server/cli.py +274 -0
- mcp_kernel_server/resources.py +217 -0
- mcp_kernel_server/server.py +564 -0
- mcp_kernel_server/tools.py +1174 -0
- mute_agent/__init__.py +68 -0
- mute_agent/core/__init__.py +1 -0
- mute_agent/core/execution_agent.py +166 -0
- mute_agent/core/handshake_protocol.py +201 -0
- mute_agent/core/reasoning_agent.py +238 -0
- mute_agent/knowledge_graph/__init__.py +1 -0
- mute_agent/knowledge_graph/graph_elements.py +65 -0
- mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
- mute_agent/knowledge_graph/subgraph.py +224 -0
- mute_agent/listener/__init__.py +43 -0
- mute_agent/listener/adapters/__init__.py +31 -0
- mute_agent/listener/adapters/base_adapter.py +189 -0
- mute_agent/listener/adapters/caas_adapter.py +344 -0
- mute_agent/listener/adapters/control_plane_adapter.py +436 -0
- mute_agent/listener/adapters/iatp_adapter.py +332 -0
- mute_agent/listener/adapters/scak_adapter.py +251 -0
- mute_agent/listener/listener.py +610 -0
- mute_agent/listener/state_observer.py +436 -0
- mute_agent/listener/threshold_config.py +313 -0
- mute_agent/super_system/__init__.py +1 -0
- mute_agent/super_system/router.py +204 -0
- mute_agent/visualization/__init__.py +10 -0
- mute_agent/visualization/graph_debugger.py +502 -0
- nexus/README.md +60 -0
- nexus/__init__.py +51 -0
- nexus/arbiter.py +359 -0
- nexus/client.py +466 -0
- nexus/dmz.py +444 -0
- nexus/escrow.py +430 -0
- nexus/exceptions.py +286 -0
- nexus/pyproject.toml +36 -0
- nexus/registry.py +393 -0
- nexus/reputation.py +425 -0
- nexus/schemas/__init__.py +51 -0
- nexus/schemas/compliance.py +276 -0
- nexus/schemas/escrow.py +251 -0
- nexus/schemas/manifest.py +225 -0
- nexus/schemas/receipt.py +208 -0
- nexus/tests/__init__.py +0 -0
- nexus/tests/conftest.py +146 -0
- nexus/tests/test_arbiter.py +192 -0
- nexus/tests/test_dmz.py +194 -0
- nexus/tests/test_escrow.py +276 -0
- nexus/tests/test_exceptions.py +225 -0
- nexus/tests/test_registry.py +232 -0
- nexus/tests/test_reputation.py +328 -0
- nexus/tests/test_schemas.py +295 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Graph Debugger - Visual Trace Generator for Mute Agent
|
|
5
|
+
|
|
6
|
+
Generates visual artifacts showing:
|
|
7
|
+
- Green Path: Nodes traversed successfully
|
|
8
|
+
- Red Node: The exact node where constraint failed
|
|
9
|
+
- Grey Nodes: Unreachable parts of the graph
|
|
10
|
+
|
|
11
|
+
This proves "Deterministic Safety" - showing where the agent physically
|
|
12
|
+
could not reach certain nodes because the path was severed.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Dict, List, Optional, Set, Any
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import networkx as nx
|
|
26
|
+
NETWORKX_AVAILABLE = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
NETWORKX_AVAILABLE = False
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from pyvis.network import Network
|
|
32
|
+
PYVIS_AVAILABLE = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
PYVIS_AVAILABLE = False
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
import matplotlib.pyplot as plt
|
|
38
|
+
import matplotlib.patches as mpatches
|
|
39
|
+
MATPLOTLIB_AVAILABLE = True
|
|
40
|
+
except ImportError:
|
|
41
|
+
MATPLOTLIB_AVAILABLE = False
|
|
42
|
+
|
|
43
|
+
VISUALIZATION_AVAILABLE = NETWORKX_AVAILABLE and (PYVIS_AVAILABLE or MATPLOTLIB_AVAILABLE)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NodeState(Enum):
|
|
47
|
+
"""States a node can be in during execution."""
|
|
48
|
+
SUCCESS = "success" # Green - traversed successfully
|
|
49
|
+
FAILURE = "failure" # Red - constraint failed here
|
|
50
|
+
UNREACHABLE = "unreachable" # Grey - could not be reached
|
|
51
|
+
PENDING = "pending" # Not yet visited
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ExecutionTrace:
|
|
56
|
+
"""Tracks execution path through the knowledge graph."""
|
|
57
|
+
session_id: str
|
|
58
|
+
action_id: str
|
|
59
|
+
traversed_nodes: List[str] = field(default_factory=list)
|
|
60
|
+
failed_node: Optional[str] = None
|
|
61
|
+
node_states: Dict[str, NodeState] = field(default_factory=dict)
|
|
62
|
+
edge_traversals: List[tuple] = field(default_factory=list)
|
|
63
|
+
validation_errors: List[str] = field(default_factory=list)
|
|
64
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
65
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class GraphDebugger:
|
|
69
|
+
"""
|
|
70
|
+
The Graph Debugger generates visual artifacts for every execution.
|
|
71
|
+
Shows deterministic safety by visualizing which paths were accessible.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, knowledge_graph=None):
|
|
75
|
+
"""
|
|
76
|
+
Initialize the Graph Debugger.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
knowledge_graph: The MultidimensionalKnowledgeGraph to visualize
|
|
80
|
+
"""
|
|
81
|
+
self.knowledge_graph = knowledge_graph
|
|
82
|
+
self.traces: List[ExecutionTrace] = []
|
|
83
|
+
|
|
84
|
+
if not VISUALIZATION_AVAILABLE:
|
|
85
|
+
missing = []
|
|
86
|
+
if not NETWORKX_AVAILABLE:
|
|
87
|
+
missing.append("networkx")
|
|
88
|
+
if not PYVIS_AVAILABLE:
|
|
89
|
+
missing.append("pyvis")
|
|
90
|
+
if not MATPLOTLIB_AVAILABLE:
|
|
91
|
+
missing.append("matplotlib")
|
|
92
|
+
print(f"Warning: Visualization libraries not available: {', '.join(missing)}")
|
|
93
|
+
print("Install with: pip install matplotlib networkx pyvis")
|
|
94
|
+
|
|
95
|
+
def create_trace(
|
|
96
|
+
self,
|
|
97
|
+
session_id: str,
|
|
98
|
+
action_id: str,
|
|
99
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
100
|
+
) -> ExecutionTrace:
|
|
101
|
+
"""Create a new execution trace."""
|
|
102
|
+
trace = ExecutionTrace(
|
|
103
|
+
session_id=session_id,
|
|
104
|
+
action_id=action_id,
|
|
105
|
+
metadata=metadata or {}
|
|
106
|
+
)
|
|
107
|
+
self.traces.append(trace)
|
|
108
|
+
return trace
|
|
109
|
+
|
|
110
|
+
def record_node_visit(self, trace: ExecutionTrace, node_id: str, success: bool):
|
|
111
|
+
"""Record a node visit in the trace."""
|
|
112
|
+
trace.traversed_nodes.append(node_id)
|
|
113
|
+
if success:
|
|
114
|
+
trace.node_states[node_id] = NodeState.SUCCESS
|
|
115
|
+
else:
|
|
116
|
+
trace.node_states[node_id] = NodeState.FAILURE
|
|
117
|
+
trace.failed_node = node_id
|
|
118
|
+
|
|
119
|
+
def record_edge_traversal(self, trace: ExecutionTrace, source_id: str, target_id: str):
|
|
120
|
+
"""Record an edge traversal."""
|
|
121
|
+
trace.edge_traversals.append((source_id, target_id))
|
|
122
|
+
|
|
123
|
+
def mark_unreachable_nodes(self, trace: ExecutionTrace, all_node_ids: List[str]):
|
|
124
|
+
"""Mark nodes that were not reached as unreachable."""
|
|
125
|
+
reached = set(trace.traversed_nodes)
|
|
126
|
+
for node_id in all_node_ids:
|
|
127
|
+
if node_id not in reached and node_id not in trace.node_states:
|
|
128
|
+
trace.node_states[node_id] = NodeState.UNREACHABLE
|
|
129
|
+
|
|
130
|
+
def visualize_trace(
|
|
131
|
+
self,
|
|
132
|
+
trace: ExecutionTrace,
|
|
133
|
+
output_path: str = "execution_trace.html",
|
|
134
|
+
format: str = "html"
|
|
135
|
+
) -> str:
|
|
136
|
+
"""
|
|
137
|
+
Generate a visual artifact for the execution trace.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
trace: The execution trace to visualize
|
|
141
|
+
output_path: Where to save the visualization
|
|
142
|
+
format: 'html' (interactive) or 'png' (static image)
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Path to the generated visualization
|
|
146
|
+
"""
|
|
147
|
+
if not VISUALIZATION_AVAILABLE:
|
|
148
|
+
print("Cannot generate visualization: libraries not installed")
|
|
149
|
+
return ""
|
|
150
|
+
|
|
151
|
+
if format == "html":
|
|
152
|
+
return self._generate_interactive_html(trace, output_path)
|
|
153
|
+
elif format == "png":
|
|
154
|
+
return self._generate_static_png(trace, output_path)
|
|
155
|
+
else:
|
|
156
|
+
raise ValueError(f"Unsupported format: {format}")
|
|
157
|
+
|
|
158
|
+
def _generate_interactive_html(self, trace: ExecutionTrace, output_path: str) -> str:
|
|
159
|
+
"""Generate an interactive HTML visualization using pyvis."""
|
|
160
|
+
# Create a directed graph
|
|
161
|
+
net = Network(
|
|
162
|
+
height="750px",
|
|
163
|
+
width="100%",
|
|
164
|
+
directed=True,
|
|
165
|
+
notebook=False,
|
|
166
|
+
bgcolor="#ffffff",
|
|
167
|
+
font_color="#000000"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Configure physics for better layout
|
|
171
|
+
net.set_options("""
|
|
172
|
+
{
|
|
173
|
+
"physics": {
|
|
174
|
+
"enabled": true,
|
|
175
|
+
"barnesHut": {
|
|
176
|
+
"gravitationalConstant": -8000,
|
|
177
|
+
"centralGravity": 0.3,
|
|
178
|
+
"springLength": 150,
|
|
179
|
+
"springConstant": 0.04
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"nodes": {
|
|
183
|
+
"font": {
|
|
184
|
+
"size": 16,
|
|
185
|
+
"face": "arial"
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"edges": {
|
|
189
|
+
"arrows": {
|
|
190
|
+
"to": {
|
|
191
|
+
"enabled": true,
|
|
192
|
+
"scaleFactor": 0.5
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"smooth": {
|
|
196
|
+
"type": "continuous"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
""")
|
|
201
|
+
|
|
202
|
+
# Add nodes with colors based on state
|
|
203
|
+
for node_id, state in trace.node_states.items():
|
|
204
|
+
color, title = self._get_node_style(node_id, state, trace)
|
|
205
|
+
|
|
206
|
+
# Make failed node stand out
|
|
207
|
+
if state == NodeState.FAILURE:
|
|
208
|
+
net.add_node(
|
|
209
|
+
node_id,
|
|
210
|
+
label=node_id,
|
|
211
|
+
color=color,
|
|
212
|
+
title=title,
|
|
213
|
+
size=30,
|
|
214
|
+
borderWidth=3,
|
|
215
|
+
borderWidthSelected=4
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
net.add_node(
|
|
219
|
+
node_id,
|
|
220
|
+
label=node_id,
|
|
221
|
+
color=color,
|
|
222
|
+
title=title,
|
|
223
|
+
size=20
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Add edges from knowledge graph if available
|
|
227
|
+
if self.knowledge_graph:
|
|
228
|
+
self._add_edges_from_knowledge_graph(net, trace)
|
|
229
|
+
else:
|
|
230
|
+
# Add edges from trace
|
|
231
|
+
for source, target in trace.edge_traversals:
|
|
232
|
+
# Check if edge was part of success or failure path
|
|
233
|
+
edge_color = "#2ecc71" if source in trace.traversed_nodes else "#95a5a6"
|
|
234
|
+
net.add_edge(source, target, color=edge_color, width=2)
|
|
235
|
+
|
|
236
|
+
# Add legend as HTML title
|
|
237
|
+
html_legend = self._create_html_legend(trace)
|
|
238
|
+
|
|
239
|
+
# Generate the HTML
|
|
240
|
+
net.save_graph(output_path)
|
|
241
|
+
|
|
242
|
+
# Insert legend into HTML
|
|
243
|
+
self._inject_legend_into_html(output_path, html_legend)
|
|
244
|
+
|
|
245
|
+
print(f"Interactive visualization saved to: {output_path}")
|
|
246
|
+
return output_path
|
|
247
|
+
|
|
248
|
+
def _generate_static_png(self, trace: ExecutionTrace, output_path: str) -> str:
|
|
249
|
+
"""Generate a static PNG visualization using matplotlib and networkx."""
|
|
250
|
+
# Create a directed graph
|
|
251
|
+
G = nx.DiGraph()
|
|
252
|
+
|
|
253
|
+
# Add nodes
|
|
254
|
+
for node_id in trace.node_states.keys():
|
|
255
|
+
G.add_node(node_id)
|
|
256
|
+
|
|
257
|
+
# Add edges
|
|
258
|
+
if self.knowledge_graph:
|
|
259
|
+
# Extract edges from knowledge graph
|
|
260
|
+
for dim_name, subgraph in self.knowledge_graph.subgraphs.items():
|
|
261
|
+
for edge in subgraph.edges:
|
|
262
|
+
if edge.source_id in G.nodes and edge.target_id in G.nodes:
|
|
263
|
+
G.add_edge(edge.source_id, edge.target_id)
|
|
264
|
+
else:
|
|
265
|
+
for source, target in trace.edge_traversals:
|
|
266
|
+
G.add_edge(source, target)
|
|
267
|
+
|
|
268
|
+
# Create figure
|
|
269
|
+
plt.figure(figsize=(14, 10))
|
|
270
|
+
|
|
271
|
+
# Use hierarchical layout
|
|
272
|
+
try:
|
|
273
|
+
pos = nx.spring_layout(G, k=2, iterations=50)
|
|
274
|
+
except (ValueError, RuntimeError) as e:
|
|
275
|
+
logger.debug("spring_layout failed, falling back to shell_layout: %s", e)
|
|
276
|
+
pos = nx.shell_layout(G)
|
|
277
|
+
|
|
278
|
+
# Draw nodes with colors based on state
|
|
279
|
+
for state in NodeState:
|
|
280
|
+
nodes = [n for n, s in trace.node_states.items() if s == state]
|
|
281
|
+
if nodes:
|
|
282
|
+
color, label = self._get_node_color_for_png(state)
|
|
283
|
+
nx.draw_networkx_nodes(
|
|
284
|
+
G, pos,
|
|
285
|
+
nodelist=nodes,
|
|
286
|
+
node_color=color,
|
|
287
|
+
node_size=1000 if state == NodeState.FAILURE else 700,
|
|
288
|
+
label=label,
|
|
289
|
+
alpha=0.9
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Draw edges
|
|
293
|
+
nx.draw_networkx_edges(
|
|
294
|
+
G, pos,
|
|
295
|
+
edge_color='#95a5a6',
|
|
296
|
+
arrows=True,
|
|
297
|
+
arrowsize=20,
|
|
298
|
+
width=2,
|
|
299
|
+
alpha=0.6
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Highlight traversed edges
|
|
303
|
+
traversed_edges = [(s, t) for s, t in trace.edge_traversals if G.has_edge(s, t)]
|
|
304
|
+
if traversed_edges:
|
|
305
|
+
nx.draw_networkx_edges(
|
|
306
|
+
G, pos,
|
|
307
|
+
edgelist=traversed_edges,
|
|
308
|
+
edge_color='#2ecc71',
|
|
309
|
+
arrows=True,
|
|
310
|
+
arrowsize=20,
|
|
311
|
+
width=3,
|
|
312
|
+
alpha=0.9
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Draw labels
|
|
316
|
+
nx.draw_networkx_labels(G, pos, font_size=10, font_weight='bold')
|
|
317
|
+
|
|
318
|
+
# Add title and legend
|
|
319
|
+
plt.title(
|
|
320
|
+
f"Execution Trace: {trace.action_id}\n"
|
|
321
|
+
f"Session: {trace.session_id}",
|
|
322
|
+
fontsize=14,
|
|
323
|
+
fontweight='bold',
|
|
324
|
+
pad=20
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
plt.legend(loc='upper left', fontsize=10)
|
|
328
|
+
plt.axis('off')
|
|
329
|
+
plt.tight_layout()
|
|
330
|
+
|
|
331
|
+
# Save
|
|
332
|
+
plt.savefig(output_path, dpi=300, bbox_inches='tight', facecolor='white')
|
|
333
|
+
plt.close()
|
|
334
|
+
|
|
335
|
+
print(f"Static visualization saved to: {output_path}")
|
|
336
|
+
return output_path
|
|
337
|
+
|
|
338
|
+
def _get_node_style(self, node_id: str, state: NodeState, trace: ExecutionTrace) -> tuple:
|
|
339
|
+
"""Get node color and title based on state."""
|
|
340
|
+
if state == NodeState.SUCCESS:
|
|
341
|
+
color = "#2ecc71" # Green
|
|
342
|
+
title = f"{node_id}\n✓ Traversed successfully"
|
|
343
|
+
elif state == NodeState.FAILURE:
|
|
344
|
+
color = "#e74c3c" # Red
|
|
345
|
+
errors = "\n".join(trace.validation_errors) if trace.validation_errors else "Constraint failed"
|
|
346
|
+
title = f"{node_id}\n✗ FAILED\n{errors}"
|
|
347
|
+
elif state == NodeState.UNREACHABLE:
|
|
348
|
+
color = "#95a5a6" # Grey
|
|
349
|
+
title = f"{node_id}\n○ Unreachable (path severed)"
|
|
350
|
+
else:
|
|
351
|
+
color = "#3498db" # Blue
|
|
352
|
+
title = f"{node_id}\n○ Pending"
|
|
353
|
+
|
|
354
|
+
return color, title
|
|
355
|
+
|
|
356
|
+
def _get_node_color_for_png(self, state: NodeState) -> tuple:
|
|
357
|
+
"""Get node color and label for PNG visualization."""
|
|
358
|
+
if state == NodeState.SUCCESS:
|
|
359
|
+
return "#2ecc71", "Success (Traversed)"
|
|
360
|
+
elif state == NodeState.FAILURE:
|
|
361
|
+
return "#e74c3c", "Failure (Constraint Violated)"
|
|
362
|
+
elif state == NodeState.UNREACHABLE:
|
|
363
|
+
return "#95a5a6", "Unreachable (Path Severed)"
|
|
364
|
+
else:
|
|
365
|
+
return "#3498db", "Pending"
|
|
366
|
+
|
|
367
|
+
def _add_edges_from_knowledge_graph(self, net, trace: ExecutionTrace):
|
|
368
|
+
"""Add edges from the knowledge graph to the visualization."""
|
|
369
|
+
if not self.knowledge_graph:
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
traversed = set(trace.traversed_nodes)
|
|
373
|
+
|
|
374
|
+
for dim_name, subgraph in self.knowledge_graph.subgraphs.items():
|
|
375
|
+
for edge in subgraph.edges:
|
|
376
|
+
# Only add edges between nodes that are in the trace
|
|
377
|
+
if edge.source_id in trace.node_states and edge.target_id in trace.node_states:
|
|
378
|
+
# Color based on whether edge was traversed
|
|
379
|
+
if edge.source_id in traversed:
|
|
380
|
+
color = "#2ecc71" # Green for traversed
|
|
381
|
+
width = 3
|
|
382
|
+
else:
|
|
383
|
+
color = "#95a5a6" # Grey for not traversed
|
|
384
|
+
width = 1
|
|
385
|
+
|
|
386
|
+
title = f"{edge.edge_type.value}: {edge.source_id} → {edge.target_id}"
|
|
387
|
+
net.add_edge(
|
|
388
|
+
edge.source_id,
|
|
389
|
+
edge.target_id,
|
|
390
|
+
color=color,
|
|
391
|
+
width=width,
|
|
392
|
+
title=title
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def _create_html_legend(self, trace: ExecutionTrace) -> str:
|
|
396
|
+
"""Create HTML legend for the visualization."""
|
|
397
|
+
success_count = sum(1 for s in trace.node_states.values() if s == NodeState.SUCCESS)
|
|
398
|
+
failure_count = sum(1 for s in trace.node_states.values() if s == NodeState.FAILURE)
|
|
399
|
+
unreachable_count = sum(1 for s in trace.node_states.values() if s == NodeState.UNREACHABLE)
|
|
400
|
+
|
|
401
|
+
legend = f"""
|
|
402
|
+
<div style="position: absolute; top: 10px; right: 10px; background: white;
|
|
403
|
+
padding: 15px; border: 2px solid #ccc; border-radius: 5px;
|
|
404
|
+
font-family: Arial; max-width: 300px;">
|
|
405
|
+
<h3 style="margin: 0 0 10px 0;">Execution Trace Legend</h3>
|
|
406
|
+
<p style="margin: 5px 0;"><span style="color: #2ecc71;">●</span> <strong>Green:</strong> Traversed successfully ({success_count})</p>
|
|
407
|
+
<p style="margin: 5px 0;"><span style="color: #e74c3c;">●</span> <strong>Red:</strong> Constraint failed ({failure_count})</p>
|
|
408
|
+
<p style="margin: 5px 0;"><span style="color: #95a5a6;">●</span> <strong>Grey:</strong> Unreachable ({unreachable_count})</p>
|
|
409
|
+
<hr style="margin: 10px 0;">
|
|
410
|
+
<p style="margin: 5px 0; font-size: 12px;"><strong>Session:</strong> {trace.session_id}</p>
|
|
411
|
+
<p style="margin: 5px 0; font-size: 12px;"><strong>Action:</strong> {trace.action_id}</p>
|
|
412
|
+
{f'<p style="margin: 5px 0; font-size: 12px; color: #e74c3c;"><strong>Failed at:</strong> {trace.failed_node}</p>' if trace.failed_node else ''}
|
|
413
|
+
</div>
|
|
414
|
+
"""
|
|
415
|
+
return legend
|
|
416
|
+
|
|
417
|
+
def _inject_legend_into_html(self, html_path: str, legend_html: str):
|
|
418
|
+
"""Inject legend HTML into the generated visualization."""
|
|
419
|
+
try:
|
|
420
|
+
with open(html_path, 'r') as f:
|
|
421
|
+
html_content = f.read()
|
|
422
|
+
|
|
423
|
+
# Insert legend before closing body tag
|
|
424
|
+
html_content = html_content.replace('</body>', f'{legend_html}</body>')
|
|
425
|
+
|
|
426
|
+
with open(html_path, 'w') as f:
|
|
427
|
+
f.write(html_content)
|
|
428
|
+
except FileNotFoundError:
|
|
429
|
+
print(f"Warning: Could not find HTML file: {html_path}")
|
|
430
|
+
except PermissionError:
|
|
431
|
+
print(f"Warning: Permission denied writing to: {html_path}")
|
|
432
|
+
except Exception as e:
|
|
433
|
+
print(f"Warning: Could not inject legend: {type(e).__name__}: {e}")
|
|
434
|
+
|
|
435
|
+
def generate_comparison_visualization(
|
|
436
|
+
self,
|
|
437
|
+
traces: List[ExecutionTrace],
|
|
438
|
+
output_path: str = "trace_comparison.png"
|
|
439
|
+
) -> str:
|
|
440
|
+
"""
|
|
441
|
+
Generate a side-by-side comparison of multiple traces.
|
|
442
|
+
Useful for showing how different contexts lead to different paths.
|
|
443
|
+
"""
|
|
444
|
+
if not VISUALIZATION_AVAILABLE:
|
|
445
|
+
print("Cannot generate visualization: libraries not installed")
|
|
446
|
+
return ""
|
|
447
|
+
|
|
448
|
+
num_traces = len(traces)
|
|
449
|
+
fig, axes = plt.subplots(1, num_traces, figsize=(7 * num_traces, 6))
|
|
450
|
+
|
|
451
|
+
if num_traces == 1:
|
|
452
|
+
axes = [axes]
|
|
453
|
+
|
|
454
|
+
for idx, trace in enumerate(traces):
|
|
455
|
+
ax = axes[idx]
|
|
456
|
+
|
|
457
|
+
# Create graph for this trace
|
|
458
|
+
G = nx.DiGraph()
|
|
459
|
+
for node_id in trace.node_states.keys():
|
|
460
|
+
G.add_node(node_id)
|
|
461
|
+
for source, target in trace.edge_traversals:
|
|
462
|
+
G.add_edge(source, target)
|
|
463
|
+
|
|
464
|
+
# Layout
|
|
465
|
+
try:
|
|
466
|
+
pos = nx.spring_layout(G, k=1.5, iterations=50)
|
|
467
|
+
except (ValueError, RuntimeError) as e:
|
|
468
|
+
logger.debug("spring_layout failed, falling back to shell_layout: %s", e)
|
|
469
|
+
pos = nx.shell_layout(G)
|
|
470
|
+
|
|
471
|
+
# Draw nodes
|
|
472
|
+
for state in NodeState:
|
|
473
|
+
nodes = [n for n, s in trace.node_states.items() if s == state]
|
|
474
|
+
if nodes:
|
|
475
|
+
color, _ = self._get_node_color_for_png(state)
|
|
476
|
+
nx.draw_networkx_nodes(
|
|
477
|
+
G, pos,
|
|
478
|
+
nodelist=nodes,
|
|
479
|
+
node_color=color,
|
|
480
|
+
node_size=500,
|
|
481
|
+
alpha=0.9,
|
|
482
|
+
ax=ax
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Draw edges
|
|
486
|
+
nx.draw_networkx_edges(G, pos, edge_color='#95a5a6',
|
|
487
|
+
arrows=True, arrowsize=15, width=1.5,
|
|
488
|
+
alpha=0.6, ax=ax)
|
|
489
|
+
|
|
490
|
+
# Draw labels
|
|
491
|
+
nx.draw_networkx_labels(G, pos, font_size=8, font_weight='bold', ax=ax)
|
|
492
|
+
|
|
493
|
+
ax.set_title(f"Trace {idx + 1}: {trace.action_id}", fontweight='bold')
|
|
494
|
+
ax.axis('off')
|
|
495
|
+
|
|
496
|
+
plt.suptitle("Execution Trace Comparison", fontsize=16, fontweight='bold')
|
|
497
|
+
plt.tight_layout()
|
|
498
|
+
plt.savefig(output_path, dpi=300, bbox_inches='tight', facecolor='white')
|
|
499
|
+
plt.close()
|
|
500
|
+
|
|
501
|
+
print(f"Comparison visualization saved to: {output_path}")
|
|
502
|
+
return output_path
|
nexus/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Nexus Trust Exchange
|
|
2
|
+
|
|
3
|
+
**Agent Trust Exchange — viral registry and communication board for AI agents.**
|
|
4
|
+
|
|
5
|
+
> ⚠️ **RESEARCH PROTOTYPE** — This module is in pre-alpha. Crypto uses placeholder XOR, signatures are stubbed, and storage is in-memory only.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Nexus provides a decentralized trust exchange layer for AI agent ecosystems. It enables agents to:
|
|
10
|
+
|
|
11
|
+
- **Register** capabilities and identity on a shared registry
|
|
12
|
+
- **Exchange** trust attestations with other agents
|
|
13
|
+
- **Arbitrate** disputes through an escrow/arbiter system
|
|
14
|
+
- **Build reputation** via a weighted reputation graph
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install nexus-trust-exchange
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Components
|
|
23
|
+
|
|
24
|
+
| Module | Purpose |
|
|
25
|
+
|--------|---------|
|
|
26
|
+
| `registry.py` | Agent registration and capability discovery |
|
|
27
|
+
| `client.py` | Client SDK for interacting with the exchange |
|
|
28
|
+
| `arbiter.py` | Trust dispute resolution |
|
|
29
|
+
| `escrow.py` | Conditional trust escrow |
|
|
30
|
+
| `dmz.py` | Demilitarized zone for untrusted agent interaction |
|
|
31
|
+
| `reputation.py` | Reputation scoring and graph |
|
|
32
|
+
| `schemas/` | Pydantic models for all exchange messages |
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from nexus import NexusRegistry, NexusClient
|
|
38
|
+
|
|
39
|
+
# Create a registry
|
|
40
|
+
registry = NexusRegistry()
|
|
41
|
+
|
|
42
|
+
# Register an agent
|
|
43
|
+
registry.register_agent(
|
|
44
|
+
agent_id="agent-001",
|
|
45
|
+
capabilities=["code-review", "testing"],
|
|
46
|
+
trust_level=0.8
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Part of Agent-OS
|
|
51
|
+
|
|
52
|
+
This module is part of the [Agent-OS](https://github.com/microsoft/agent-governance-toolkit) ecosystem. Install the full stack:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install agent-os-kernel
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
nexus/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Nexus: The Agent Trust Exchange
|
|
5
|
+
|
|
6
|
+
A viral, cloud-based registry and communication board that uses the Agent OS
|
|
7
|
+
kernel to enforce trust. Serves as a neutral ground where agents can discover
|
|
8
|
+
each other, negotiate contracts via IATP, and settle rewards for successful tasks.
|
|
9
|
+
|
|
10
|
+
The "Visa Network" for AI Agents.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .client import NexusClient
|
|
14
|
+
from .registry import AgentRegistry
|
|
15
|
+
from .reputation import ReputationEngine, TrustScore
|
|
16
|
+
from .escrow import ProofOfOutcome, EscrowManager
|
|
17
|
+
from .arbiter import Arbiter, DisputeResolution
|
|
18
|
+
from .dmz import DMZProtocol, DataHandlingPolicy
|
|
19
|
+
from .exceptions import (
|
|
20
|
+
NexusError,
|
|
21
|
+
IATPUnverifiedPeerException,
|
|
22
|
+
IATPInsufficientTrustException,
|
|
23
|
+
EscrowError,
|
|
24
|
+
DisputeError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__version__ = "3.1.0"
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Client
|
|
30
|
+
"NexusClient",
|
|
31
|
+
# Registry
|
|
32
|
+
"AgentRegistry",
|
|
33
|
+
# Reputation
|
|
34
|
+
"ReputationEngine",
|
|
35
|
+
"TrustScore",
|
|
36
|
+
# Escrow
|
|
37
|
+
"ProofOfOutcome",
|
|
38
|
+
"EscrowManager",
|
|
39
|
+
# Arbiter
|
|
40
|
+
"Arbiter",
|
|
41
|
+
"DisputeResolution",
|
|
42
|
+
# DMZ
|
|
43
|
+
"DMZProtocol",
|
|
44
|
+
"DataHandlingPolicy",
|
|
45
|
+
# Exceptions
|
|
46
|
+
"NexusError",
|
|
47
|
+
"IATPUnverifiedPeerException",
|
|
48
|
+
"IATPInsufficientTrustException",
|
|
49
|
+
"EscrowError",
|
|
50
|
+
"DisputeError",
|
|
51
|
+
]
|