kailash 0.8.4__py3-none-any.whl → 0.8.5__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.
- kailash/__init__.py +1 -7
- kailash/cli/__init__.py +11 -1
- kailash/cli/validation_audit.py +570 -0
- kailash/core/actors/supervisor.py +1 -1
- kailash/core/resilience/circuit_breaker.py +71 -1
- kailash/core/resilience/health_monitor.py +172 -0
- kailash/edge/compliance.py +33 -0
- kailash/edge/consistency.py +609 -0
- kailash/edge/coordination/__init__.py +30 -0
- kailash/edge/coordination/global_ordering.py +355 -0
- kailash/edge/coordination/leader_election.py +217 -0
- kailash/edge/coordination/partition_detector.py +296 -0
- kailash/edge/coordination/raft.py +485 -0
- kailash/edge/discovery.py +63 -1
- kailash/edge/migration/__init__.py +19 -0
- kailash/edge/migration/edge_migrator.py +832 -0
- kailash/edge/monitoring/__init__.py +21 -0
- kailash/edge/monitoring/edge_monitor.py +736 -0
- kailash/edge/prediction/__init__.py +10 -0
- kailash/edge/prediction/predictive_warmer.py +591 -0
- kailash/edge/resource/__init__.py +102 -0
- kailash/edge/resource/cloud_integration.py +796 -0
- kailash/edge/resource/cost_optimizer.py +949 -0
- kailash/edge/resource/docker_integration.py +919 -0
- kailash/edge/resource/kubernetes_integration.py +893 -0
- kailash/edge/resource/platform_integration.py +913 -0
- kailash/edge/resource/predictive_scaler.py +959 -0
- kailash/edge/resource/resource_analyzer.py +824 -0
- kailash/edge/resource/resource_pools.py +610 -0
- kailash/integrations/dataflow_edge.py +261 -0
- kailash/mcp_server/registry_integration.py +1 -1
- kailash/monitoring/__init__.py +18 -0
- kailash/monitoring/alerts.py +646 -0
- kailash/monitoring/metrics.py +677 -0
- kailash/nodes/__init__.py +2 -0
- kailash/nodes/ai/semantic_memory.py +2 -2
- kailash/nodes/base.py +545 -0
- kailash/nodes/edge/__init__.py +36 -0
- kailash/nodes/edge/base.py +240 -0
- kailash/nodes/edge/cloud_node.py +710 -0
- kailash/nodes/edge/coordination.py +239 -0
- kailash/nodes/edge/docker_node.py +825 -0
- kailash/nodes/edge/edge_data.py +582 -0
- kailash/nodes/edge/edge_migration_node.py +392 -0
- kailash/nodes/edge/edge_monitoring_node.py +421 -0
- kailash/nodes/edge/edge_state.py +673 -0
- kailash/nodes/edge/edge_warming_node.py +393 -0
- kailash/nodes/edge/kubernetes_node.py +652 -0
- kailash/nodes/edge/platform_node.py +766 -0
- kailash/nodes/edge/resource_analyzer_node.py +378 -0
- kailash/nodes/edge/resource_optimizer_node.py +501 -0
- kailash/nodes/edge/resource_scaler_node.py +397 -0
- kailash/nodes/ports.py +676 -0
- kailash/runtime/local.py +344 -1
- kailash/runtime/validation/__init__.py +20 -0
- kailash/runtime/validation/connection_context.py +119 -0
- kailash/runtime/validation/enhanced_error_formatter.py +202 -0
- kailash/runtime/validation/error_categorizer.py +164 -0
- kailash/runtime/validation/metrics.py +380 -0
- kailash/runtime/validation/performance.py +615 -0
- kailash/runtime/validation/suggestion_engine.py +212 -0
- kailash/testing/fixtures.py +2 -2
- kailash/workflow/builder.py +230 -4
- kailash/workflow/contracts.py +418 -0
- kailash/workflow/edge_infrastructure.py +369 -0
- kailash/workflow/migration.py +3 -3
- kailash/workflow/type_inference.py +669 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/METADATA +43 -27
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/RECORD +73 -27
- kailash/nexus/__init__.py +0 -21
- kailash/nexus/cli/__init__.py +0 -5
- kailash/nexus/cli/__main__.py +0 -6
- kailash/nexus/cli/main.py +0 -176
- kailash/nexus/factory.py +0 -413
- kailash/nexus/gateway.py +0 -545
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/WHEEL +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/entry_points.txt +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/top_level.txt +0 -0
kailash/runtime/local.py
CHANGED
@@ -37,13 +37,21 @@ Examples:
|
|
37
37
|
import asyncio
|
38
38
|
import logging
|
39
39
|
from datetime import UTC, datetime
|
40
|
-
from typing import Any, Optional
|
40
|
+
from typing import Any, Dict, Optional
|
41
41
|
|
42
42
|
import networkx as nx
|
43
43
|
|
44
44
|
from kailash.nodes import Node
|
45
45
|
from kailash.runtime.parameter_injector import WorkflowParameterInjector
|
46
46
|
from kailash.runtime.secret_provider import EnvironmentSecretProvider, SecretProvider
|
47
|
+
from kailash.runtime.validation.connection_context import ConnectionContext
|
48
|
+
from kailash.runtime.validation.enhanced_error_formatter import EnhancedErrorFormatter
|
49
|
+
from kailash.runtime.validation.error_categorizer import ErrorCategorizer
|
50
|
+
from kailash.runtime.validation.metrics import (
|
51
|
+
ValidationEventType,
|
52
|
+
get_metrics_collector,
|
53
|
+
)
|
54
|
+
from kailash.runtime.validation.suggestion_engine import ValidationSuggestionEngine
|
47
55
|
from kailash.sdk_exceptions import (
|
48
56
|
RuntimeExecutionError,
|
49
57
|
WorkflowExecutionError,
|
@@ -53,6 +61,7 @@ from kailash.tracking import TaskManager, TaskStatus
|
|
53
61
|
from kailash.tracking.metrics_collector import MetricsCollector
|
54
62
|
from kailash.tracking.models import TaskMetrics
|
55
63
|
from kailash.workflow import Workflow
|
64
|
+
from kailash.workflow.contracts import ConnectionContract, ContractValidator
|
56
65
|
from kailash.workflow.cyclic_runner import CyclicWorkflowExecutor
|
57
66
|
|
58
67
|
logger = logging.getLogger(__name__)
|
@@ -86,6 +95,7 @@ class LocalRuntime:
|
|
86
95
|
enable_audit: bool = False,
|
87
96
|
resource_limits: Optional[dict[str, Any]] = None,
|
88
97
|
secret_provider: Optional[Any] = None,
|
98
|
+
connection_validation: str = "warn",
|
89
99
|
):
|
90
100
|
"""Initialize the unified runtime.
|
91
101
|
|
@@ -100,7 +110,19 @@ class LocalRuntime:
|
|
100
110
|
enable_audit: Whether to enable audit logging.
|
101
111
|
resource_limits: Resource limits (memory_mb, cpu_cores, etc.).
|
102
112
|
secret_provider: Optional secret provider for runtime secret injection.
|
113
|
+
connection_validation: Connection parameter validation mode:
|
114
|
+
- "off": No validation (backward compatibility)
|
115
|
+
- "warn": Log warnings on validation errors (default)
|
116
|
+
- "strict": Raise errors on validation failures
|
103
117
|
"""
|
118
|
+
# Validate connection_validation parameter
|
119
|
+
valid_modes = {"off", "warn", "strict"}
|
120
|
+
if connection_validation not in valid_modes:
|
121
|
+
raise ValueError(
|
122
|
+
f"Invalid connection_validation mode: {connection_validation}. "
|
123
|
+
f"Must be one of: {valid_modes}"
|
124
|
+
)
|
125
|
+
|
104
126
|
self.debug = debug
|
105
127
|
self.enable_cycles = enable_cycles
|
106
128
|
self.enable_async = enable_async
|
@@ -111,6 +133,7 @@ class LocalRuntime:
|
|
111
133
|
self.enable_security = enable_security
|
112
134
|
self.enable_audit = enable_audit
|
113
135
|
self.resource_limits = resource_limits or {}
|
136
|
+
self.connection_validation = connection_validation
|
114
137
|
self.logger = logger
|
115
138
|
|
116
139
|
# Enterprise feature managers (lazy initialization)
|
@@ -403,6 +426,16 @@ class LocalRuntime:
|
|
403
426
|
except Exception as e:
|
404
427
|
self.logger.warning(f"Failed to update run status: {e}")
|
405
428
|
|
429
|
+
# Final cleanup of all node instances
|
430
|
+
for node_id, node_instance in workflow._node_instances.items():
|
431
|
+
if hasattr(node_instance, "cleanup"):
|
432
|
+
try:
|
433
|
+
await node_instance.cleanup()
|
434
|
+
except Exception as cleanup_error:
|
435
|
+
self.logger.warning(
|
436
|
+
f"Error during final cleanup of node {node_id}: {cleanup_error}"
|
437
|
+
)
|
438
|
+
|
406
439
|
return results, run_id
|
407
440
|
|
408
441
|
except WorkflowValidationError:
|
@@ -633,6 +666,15 @@ class LocalRuntime:
|
|
633
666
|
f"Node {node_id} completed successfully in {performance_metrics.duration:.3f}s"
|
634
667
|
)
|
635
668
|
|
669
|
+
# Clean up async resources if the node has a cleanup method
|
670
|
+
if hasattr(node_instance, "cleanup"):
|
671
|
+
try:
|
672
|
+
await node_instance.cleanup()
|
673
|
+
except Exception as cleanup_error:
|
674
|
+
self.logger.warning(
|
675
|
+
f"Error during node {node_id} cleanup: {cleanup_error}"
|
676
|
+
)
|
677
|
+
|
636
678
|
except Exception as e:
|
637
679
|
failed_nodes.append(node_id)
|
638
680
|
self.logger.error(f"Node {node_id} failed: {e}", exc_info=self.debug)
|
@@ -646,6 +688,15 @@ class LocalRuntime:
|
|
646
688
|
ended_at=datetime.now(UTC),
|
647
689
|
)
|
648
690
|
|
691
|
+
# Clean up async resources even on failure
|
692
|
+
if hasattr(node_instance, "cleanup"):
|
693
|
+
try:
|
694
|
+
await node_instance.cleanup()
|
695
|
+
except Exception as cleanup_error:
|
696
|
+
self.logger.warning(
|
697
|
+
f"Error during node {node_id} cleanup after failure: {cleanup_error}"
|
698
|
+
)
|
699
|
+
|
649
700
|
# Determine if we should continue
|
650
701
|
if self._should_stop_on_error(workflow, node_id):
|
651
702
|
error_msg = f"Node '{node_id}' failed: {e}"
|
@@ -816,8 +867,232 @@ class LocalRuntime:
|
|
816
867
|
# Apply parameter overrides
|
817
868
|
inputs.update(parameters)
|
818
869
|
|
870
|
+
# Connection parameter validation (TODO-121) with enhanced error messages and metrics
|
871
|
+
if self.connection_validation != "off":
|
872
|
+
metrics_collector = get_metrics_collector()
|
873
|
+
node_type = type(node_instance).__name__
|
874
|
+
|
875
|
+
# Start metrics collection
|
876
|
+
metrics_collector.start_validation(
|
877
|
+
node_id, node_type, self.connection_validation
|
878
|
+
)
|
879
|
+
|
880
|
+
try:
|
881
|
+
# Phase 2: Contract validation (if contracts exist in workflow metadata)
|
882
|
+
contract_violations = self._validate_connection_contracts(
|
883
|
+
workflow, node_id, inputs, node_outputs
|
884
|
+
)
|
885
|
+
|
886
|
+
if contract_violations:
|
887
|
+
contract_error_msg = "\n".join(
|
888
|
+
[
|
889
|
+
f"Contract '{violation['contract']}' violation on connection {violation['connection']}: {violation['error']}"
|
890
|
+
for violation in contract_violations
|
891
|
+
]
|
892
|
+
)
|
893
|
+
raise WorkflowExecutionError(
|
894
|
+
f"Connection contract validation failed for node '{node_id}': {contract_error_msg}"
|
895
|
+
)
|
896
|
+
|
897
|
+
# Merge node config with inputs before validation (matches node.execute behavior)
|
898
|
+
# This ensures connection validation considers both runtime inputs AND node configuration
|
899
|
+
merged_inputs = {**node_instance.config, **inputs}
|
900
|
+
|
901
|
+
# Handle nested config case (same as in node.execute)
|
902
|
+
if "config" in merged_inputs and isinstance(
|
903
|
+
merged_inputs["config"], dict
|
904
|
+
):
|
905
|
+
nested_config = merged_inputs["config"]
|
906
|
+
for key, value in nested_config.items():
|
907
|
+
if key not in inputs: # Runtime inputs take precedence
|
908
|
+
merged_inputs[key] = value
|
909
|
+
|
910
|
+
# Use the node's existing validate_inputs method with merged inputs
|
911
|
+
validated_inputs = node_instance.validate_inputs(**merged_inputs)
|
912
|
+
|
913
|
+
# Extract only the runtime inputs from validated results
|
914
|
+
# (exclude config parameters that were merged for validation)
|
915
|
+
validated_runtime_inputs = {}
|
916
|
+
for key, value in validated_inputs.items():
|
917
|
+
# Include if it was in original inputs OR not in node config
|
918
|
+
# This preserves validated/converted values from runtime inputs
|
919
|
+
if key in inputs or key not in node_instance.config:
|
920
|
+
validated_runtime_inputs[key] = value
|
921
|
+
|
922
|
+
# Record successful validation
|
923
|
+
metrics_collector.end_validation(node_id, node_type, success=True)
|
924
|
+
|
925
|
+
# Replace inputs with validated runtime inputs only
|
926
|
+
inputs = validated_runtime_inputs
|
927
|
+
|
928
|
+
except Exception as e:
|
929
|
+
# Categorize the error for metrics
|
930
|
+
categorizer = ErrorCategorizer()
|
931
|
+
error_category = categorizer.categorize_error(e, node_type)
|
932
|
+
|
933
|
+
# Build connection info for metrics
|
934
|
+
connection_info = {"source": "unknown", "target": node_id}
|
935
|
+
for connection in workflow.connections:
|
936
|
+
if connection.target_node == node_id:
|
937
|
+
connection_info["source"] = connection.source_node
|
938
|
+
break
|
939
|
+
|
940
|
+
# Record failed validation
|
941
|
+
metrics_collector.end_validation(
|
942
|
+
node_id,
|
943
|
+
node_type,
|
944
|
+
success=False,
|
945
|
+
error_category=error_category,
|
946
|
+
connection_info=connection_info,
|
947
|
+
)
|
948
|
+
|
949
|
+
# Check for security violations
|
950
|
+
if error_category.value == "security_violation":
|
951
|
+
metrics_collector.record_security_violation(
|
952
|
+
node_id,
|
953
|
+
node_type,
|
954
|
+
{"message": str(e), "category": "connection_validation"},
|
955
|
+
connection_info,
|
956
|
+
)
|
957
|
+
|
958
|
+
# Generate enhanced error message with connection tracing
|
959
|
+
error_msg = self._generate_enhanced_validation_error(
|
960
|
+
node_id, node_instance, e, workflow, parameters
|
961
|
+
)
|
962
|
+
|
963
|
+
if self.connection_validation == "strict":
|
964
|
+
# Strict mode: raise the error with enhanced message
|
965
|
+
raise WorkflowExecutionError(error_msg) from e
|
966
|
+
elif self.connection_validation == "warn":
|
967
|
+
# Warn mode: log enhanced warning and continue with unvalidated inputs
|
968
|
+
self.logger.warning(error_msg)
|
969
|
+
# Continue with original inputs
|
970
|
+
else:
|
971
|
+
# Record mode bypass for metrics
|
972
|
+
metrics_collector = get_metrics_collector()
|
973
|
+
metrics_collector.record_mode_bypass(
|
974
|
+
node_id, type(node_instance).__name__, self.connection_validation
|
975
|
+
)
|
976
|
+
|
819
977
|
return inputs
|
820
978
|
|
979
|
+
def _generate_enhanced_validation_error(
|
980
|
+
self,
|
981
|
+
node_id: str,
|
982
|
+
node_instance: Node,
|
983
|
+
original_error: Exception,
|
984
|
+
workflow: "Workflow", # Type annotation as string to avoid circular import
|
985
|
+
parameters: dict,
|
986
|
+
) -> str:
|
987
|
+
"""Generate enhanced validation error message with connection tracing and suggestions.
|
988
|
+
|
989
|
+
Args:
|
990
|
+
node_id: ID of the target node that failed validation
|
991
|
+
node_instance: The node instance that failed
|
992
|
+
original_error: Original validation exception
|
993
|
+
workflow: The workflow being executed
|
994
|
+
parameters: Runtime parameters
|
995
|
+
|
996
|
+
Returns:
|
997
|
+
Enhanced error message with connection context and actionable suggestions
|
998
|
+
"""
|
999
|
+
# Initialize error enhancement components
|
1000
|
+
categorizer = ErrorCategorizer()
|
1001
|
+
suggestion_engine = ValidationSuggestionEngine()
|
1002
|
+
formatter = EnhancedErrorFormatter()
|
1003
|
+
|
1004
|
+
# Categorize the error
|
1005
|
+
node_type = type(node_instance).__name__
|
1006
|
+
error_category = categorizer.categorize_error(original_error, node_type)
|
1007
|
+
|
1008
|
+
# Build connection context by finding the connections that feed into this node
|
1009
|
+
connection_context = self._build_connection_context(
|
1010
|
+
node_id, workflow, parameters
|
1011
|
+
)
|
1012
|
+
|
1013
|
+
# Generate suggestion for fixing the error
|
1014
|
+
suggestion = suggestion_engine.generate_suggestion(
|
1015
|
+
error_category, node_type, connection_context, str(original_error)
|
1016
|
+
)
|
1017
|
+
|
1018
|
+
# Format the enhanced error message
|
1019
|
+
if error_category.value == "security_violation":
|
1020
|
+
enhanced_msg = formatter.format_security_error(
|
1021
|
+
str(original_error), connection_context, suggestion
|
1022
|
+
)
|
1023
|
+
else:
|
1024
|
+
enhanced_msg = formatter.format_enhanced_error(
|
1025
|
+
str(original_error), error_category, connection_context, suggestion
|
1026
|
+
)
|
1027
|
+
|
1028
|
+
return enhanced_msg
|
1029
|
+
|
1030
|
+
def _build_connection_context(
|
1031
|
+
self, target_node_id: str, workflow: "Workflow", parameters: dict
|
1032
|
+
) -> ConnectionContext:
|
1033
|
+
"""Build connection context for error message enhancement.
|
1034
|
+
|
1035
|
+
Args:
|
1036
|
+
target_node_id: ID of the target node
|
1037
|
+
workflow: The workflow being executed
|
1038
|
+
parameters: Runtime parameters
|
1039
|
+
|
1040
|
+
Returns:
|
1041
|
+
ConnectionContext with source/target information
|
1042
|
+
"""
|
1043
|
+
# Find the primary connection feeding into this node
|
1044
|
+
source_node = "unknown"
|
1045
|
+
source_port = None
|
1046
|
+
target_port = "input"
|
1047
|
+
parameter_value = None
|
1048
|
+
|
1049
|
+
# Look through workflow connections to find what feeds this node
|
1050
|
+
for connection in workflow.connections:
|
1051
|
+
if connection.target_node == target_node_id:
|
1052
|
+
source_node = connection.source_node
|
1053
|
+
source_port = connection.source_output
|
1054
|
+
target_port = connection.target_input
|
1055
|
+
|
1056
|
+
# Try to get the actual parameter value from runtime parameters
|
1057
|
+
if target_port in parameters:
|
1058
|
+
parameter_value = parameters[target_port]
|
1059
|
+
break
|
1060
|
+
|
1061
|
+
# If no connection found, this might be a direct parameter issue
|
1062
|
+
if source_node == "unknown" and parameters:
|
1063
|
+
# Find the first parameter that might have caused the issue
|
1064
|
+
for key, value in parameters.items():
|
1065
|
+
parameter_value = value
|
1066
|
+
target_port = key
|
1067
|
+
break
|
1068
|
+
|
1069
|
+
return ConnectionContext(
|
1070
|
+
source_node=source_node,
|
1071
|
+
source_port=source_port,
|
1072
|
+
target_node=target_node_id,
|
1073
|
+
target_port=target_port,
|
1074
|
+
parameter_value=parameter_value,
|
1075
|
+
validation_mode=self.connection_validation,
|
1076
|
+
)
|
1077
|
+
|
1078
|
+
def get_validation_metrics(self) -> Dict[str, Any]:
|
1079
|
+
"""Get validation performance metrics for the runtime.
|
1080
|
+
|
1081
|
+
Returns:
|
1082
|
+
Dictionary containing performance and security metrics
|
1083
|
+
"""
|
1084
|
+
metrics_collector = get_metrics_collector()
|
1085
|
+
return {
|
1086
|
+
"performance_summary": metrics_collector.get_performance_summary(),
|
1087
|
+
"security_report": metrics_collector.get_security_report(),
|
1088
|
+
"raw_metrics": metrics_collector.export_metrics() if self.debug else None,
|
1089
|
+
}
|
1090
|
+
|
1091
|
+
def reset_validation_metrics(self) -> None:
|
1092
|
+
"""Reset validation metrics collector."""
|
1093
|
+
metrics_collector = get_metrics_collector()
|
1094
|
+
metrics_collector.reset_metrics()
|
1095
|
+
|
821
1096
|
def _should_stop_on_error(self, workflow: Workflow, node_id: str) -> bool:
|
822
1097
|
"""Determine if execution should stop when a node fails.
|
823
1098
|
|
@@ -1200,3 +1475,71 @@ class LocalRuntime:
|
|
1200
1475
|
|
1201
1476
|
# Default to workflow-level format
|
1202
1477
|
return False
|
1478
|
+
|
1479
|
+
def _validate_connection_contracts(
|
1480
|
+
self,
|
1481
|
+
workflow: Workflow,
|
1482
|
+
target_node_id: str,
|
1483
|
+
target_inputs: dict[str, Any],
|
1484
|
+
node_outputs: dict[str, dict[str, Any]],
|
1485
|
+
) -> list[dict[str, str]]:
|
1486
|
+
"""
|
1487
|
+
Validate connection contracts for a target node.
|
1488
|
+
|
1489
|
+
Args:
|
1490
|
+
workflow: The workflow being executed
|
1491
|
+
target_node_id: ID of the target node
|
1492
|
+
target_inputs: Inputs being passed to the target node
|
1493
|
+
node_outputs: Outputs from all previously executed nodes
|
1494
|
+
|
1495
|
+
Returns:
|
1496
|
+
List of contract violations (empty if all valid)
|
1497
|
+
"""
|
1498
|
+
violations = []
|
1499
|
+
|
1500
|
+
# Get connection contracts from workflow metadata
|
1501
|
+
connection_contracts = workflow.metadata.get("connection_contracts", {})
|
1502
|
+
if not connection_contracts:
|
1503
|
+
return violations # No contracts to validate
|
1504
|
+
|
1505
|
+
# Create contract validator
|
1506
|
+
validator = ContractValidator()
|
1507
|
+
|
1508
|
+
# Find all connections targeting this node
|
1509
|
+
for connection in workflow.connections:
|
1510
|
+
if connection.target_node == target_node_id:
|
1511
|
+
connection_id = f"{connection.source_node}.{connection.source_output} → {connection.target_node}.{connection.target_input}"
|
1512
|
+
|
1513
|
+
# Check if this connection has a contract
|
1514
|
+
if connection_id in connection_contracts:
|
1515
|
+
contract_dict = connection_contracts[connection_id]
|
1516
|
+
|
1517
|
+
# Reconstruct contract from dictionary
|
1518
|
+
contract = ConnectionContract.from_dict(contract_dict)
|
1519
|
+
|
1520
|
+
# Get source data from node outputs
|
1521
|
+
source_data = None
|
1522
|
+
if connection.source_node in node_outputs:
|
1523
|
+
source_outputs = node_outputs[connection.source_node]
|
1524
|
+
if connection.source_output in source_outputs:
|
1525
|
+
source_data = source_outputs[connection.source_output]
|
1526
|
+
|
1527
|
+
# Get target data from inputs
|
1528
|
+
target_data = target_inputs.get(connection.target_input)
|
1529
|
+
|
1530
|
+
# Validate the connection if we have data
|
1531
|
+
if source_data is not None or target_data is not None:
|
1532
|
+
is_valid, errors = validator.validate_connection(
|
1533
|
+
contract, source_data, target_data
|
1534
|
+
)
|
1535
|
+
|
1536
|
+
if not is_valid:
|
1537
|
+
violations.append(
|
1538
|
+
{
|
1539
|
+
"connection": connection_id,
|
1540
|
+
"contract": contract.name,
|
1541
|
+
"error": "; ".join(errors),
|
1542
|
+
}
|
1543
|
+
)
|
1544
|
+
|
1545
|
+
return violations
|
@@ -0,0 +1,20 @@
|
|
1
|
+
"""
|
2
|
+
Runtime validation module for enhanced connection validation.
|
3
|
+
|
4
|
+
Provides connection context tracking, error categorization, suggestion generation,
|
5
|
+
and enhanced error message formatting for better developer experience.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .connection_context import ConnectionContext
|
9
|
+
from .enhanced_error_formatter import EnhancedErrorFormatter
|
10
|
+
from .error_categorizer import ErrorCategorizer, ErrorCategory
|
11
|
+
from .suggestion_engine import ValidationSuggestion, ValidationSuggestionEngine
|
12
|
+
|
13
|
+
__all__ = [
|
14
|
+
"ConnectionContext",
|
15
|
+
"ErrorCategorizer",
|
16
|
+
"ErrorCategory",
|
17
|
+
"ValidationSuggestionEngine",
|
18
|
+
"ValidationSuggestion",
|
19
|
+
"EnhancedErrorFormatter",
|
20
|
+
]
|
@@ -0,0 +1,119 @@
|
|
1
|
+
"""
|
2
|
+
Connection context tracking for enhanced error messages.
|
3
|
+
|
4
|
+
Provides detailed information about connection sources and targets
|
5
|
+
to enable precise error message generation with connection paths.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from dataclasses import dataclass
|
9
|
+
from typing import Any, Optional
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass
|
13
|
+
class ConnectionContext:
|
14
|
+
"""Context information for a specific parameter connection.
|
15
|
+
|
16
|
+
Tracks the source and target of a connection to enable detailed
|
17
|
+
error message generation with connection path reconstruction.
|
18
|
+
"""
|
19
|
+
|
20
|
+
source_node: str
|
21
|
+
"""Source node ID in the workflow"""
|
22
|
+
|
23
|
+
source_port: Optional[str]
|
24
|
+
"""Output port/parameter from source node (e.g., 'result.data')"""
|
25
|
+
|
26
|
+
target_node: str
|
27
|
+
"""Target node ID in the workflow"""
|
28
|
+
|
29
|
+
target_port: str
|
30
|
+
"""Input parameter on target node"""
|
31
|
+
|
32
|
+
parameter_value: Any
|
33
|
+
"""The actual parameter value being passed through connection"""
|
34
|
+
|
35
|
+
validation_mode: str
|
36
|
+
"""Validation mode: 'off', 'warn', or 'strict'"""
|
37
|
+
|
38
|
+
def get_connection_path(self) -> str:
|
39
|
+
"""Generate human-readable connection path string.
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
Connection path in format: source_node.source_port → target_node.target_port
|
43
|
+
"""
|
44
|
+
source_part = self.source_node
|
45
|
+
if self.source_port:
|
46
|
+
source_part = f"{self.source_node}.{self.source_port}"
|
47
|
+
|
48
|
+
target_part = f"{self.target_node}.{self.target_port}"
|
49
|
+
|
50
|
+
return f"{source_part} → {target_part}"
|
51
|
+
|
52
|
+
def get_sanitized_value(self) -> str:
|
53
|
+
"""Get sanitized representation of parameter value for error messages.
|
54
|
+
|
55
|
+
Sanitizes sensitive information like SQL injection attempts, passwords, etc.
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
Safe string representation of the parameter value
|
59
|
+
"""
|
60
|
+
if self.parameter_value is None:
|
61
|
+
return "None"
|
62
|
+
|
63
|
+
value_str = str(self.parameter_value)
|
64
|
+
|
65
|
+
# Detect and sanitize potential security issues
|
66
|
+
security_patterns = [
|
67
|
+
"drop table",
|
68
|
+
"delete from",
|
69
|
+
"insert into",
|
70
|
+
"update set",
|
71
|
+
"union select",
|
72
|
+
"exec(",
|
73
|
+
"eval(",
|
74
|
+
"script>",
|
75
|
+
"password",
|
76
|
+
"secret",
|
77
|
+
"token",
|
78
|
+
"key",
|
79
|
+
]
|
80
|
+
|
81
|
+
for pattern in security_patterns:
|
82
|
+
if pattern.lower() in value_str.lower():
|
83
|
+
return "**SANITIZED**"
|
84
|
+
|
85
|
+
# Truncate very long values
|
86
|
+
if len(value_str) > 100:
|
87
|
+
return f"{value_str[:97]}..."
|
88
|
+
|
89
|
+
return value_str
|
90
|
+
|
91
|
+
def is_security_sensitive(self) -> bool:
|
92
|
+
"""Check if the parameter value contains security-sensitive data.
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
True if the value appears to contain sensitive information
|
96
|
+
"""
|
97
|
+
if self.parameter_value is None:
|
98
|
+
return False
|
99
|
+
|
100
|
+
value_str = str(self.parameter_value).lower()
|
101
|
+
|
102
|
+
sensitive_indicators = [
|
103
|
+
"password",
|
104
|
+
"secret",
|
105
|
+
"token",
|
106
|
+
"key",
|
107
|
+
"auth",
|
108
|
+
"drop",
|
109
|
+
"delete",
|
110
|
+
"insert",
|
111
|
+
"update",
|
112
|
+
"exec",
|
113
|
+
"union",
|
114
|
+
"select",
|
115
|
+
"script",
|
116
|
+
"eval",
|
117
|
+
]
|
118
|
+
|
119
|
+
return any(indicator in value_str for indicator in sensitive_indicators)
|