kailash 0.8.4__py3-none-any.whl → 0.8.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. kailash/__init__.py +5 -11
  2. kailash/channels/__init__.py +2 -1
  3. kailash/channels/mcp_channel.py +23 -4
  4. kailash/cli/__init__.py +11 -1
  5. kailash/cli/validate_imports.py +202 -0
  6. kailash/cli/validation_audit.py +570 -0
  7. kailash/core/actors/supervisor.py +1 -1
  8. kailash/core/resilience/bulkhead.py +15 -5
  9. kailash/core/resilience/circuit_breaker.py +74 -1
  10. kailash/core/resilience/health_monitor.py +433 -33
  11. kailash/edge/compliance.py +33 -0
  12. kailash/edge/consistency.py +609 -0
  13. kailash/edge/coordination/__init__.py +30 -0
  14. kailash/edge/coordination/global_ordering.py +355 -0
  15. kailash/edge/coordination/leader_election.py +217 -0
  16. kailash/edge/coordination/partition_detector.py +296 -0
  17. kailash/edge/coordination/raft.py +485 -0
  18. kailash/edge/discovery.py +63 -1
  19. kailash/edge/migration/__init__.py +19 -0
  20. kailash/edge/migration/edge_migration_service.py +384 -0
  21. kailash/edge/migration/edge_migrator.py +832 -0
  22. kailash/edge/monitoring/__init__.py +21 -0
  23. kailash/edge/monitoring/edge_monitor.py +736 -0
  24. kailash/edge/prediction/__init__.py +10 -0
  25. kailash/edge/prediction/predictive_warmer.py +591 -0
  26. kailash/edge/resource/__init__.py +102 -0
  27. kailash/edge/resource/cloud_integration.py +796 -0
  28. kailash/edge/resource/cost_optimizer.py +949 -0
  29. kailash/edge/resource/docker_integration.py +919 -0
  30. kailash/edge/resource/kubernetes_integration.py +893 -0
  31. kailash/edge/resource/platform_integration.py +913 -0
  32. kailash/edge/resource/predictive_scaler.py +959 -0
  33. kailash/edge/resource/resource_analyzer.py +824 -0
  34. kailash/edge/resource/resource_pools.py +610 -0
  35. kailash/integrations/dataflow_edge.py +261 -0
  36. kailash/mcp_server/registry_integration.py +1 -1
  37. kailash/mcp_server/server.py +351 -8
  38. kailash/mcp_server/transports.py +305 -0
  39. kailash/middleware/gateway/event_store.py +1 -0
  40. kailash/monitoring/__init__.py +18 -0
  41. kailash/monitoring/alerts.py +646 -0
  42. kailash/monitoring/metrics.py +677 -0
  43. kailash/nodes/__init__.py +2 -0
  44. kailash/nodes/ai/semantic_memory.py +2 -2
  45. kailash/nodes/base.py +622 -1
  46. kailash/nodes/code/python.py +44 -3
  47. kailash/nodes/data/async_sql.py +42 -20
  48. kailash/nodes/edge/__init__.py +36 -0
  49. kailash/nodes/edge/base.py +240 -0
  50. kailash/nodes/edge/cloud_node.py +710 -0
  51. kailash/nodes/edge/coordination.py +239 -0
  52. kailash/nodes/edge/docker_node.py +825 -0
  53. kailash/nodes/edge/edge_data.py +582 -0
  54. kailash/nodes/edge/edge_migration_node.py +396 -0
  55. kailash/nodes/edge/edge_monitoring_node.py +421 -0
  56. kailash/nodes/edge/edge_state.py +673 -0
  57. kailash/nodes/edge/edge_warming_node.py +393 -0
  58. kailash/nodes/edge/kubernetes_node.py +652 -0
  59. kailash/nodes/edge/platform_node.py +766 -0
  60. kailash/nodes/edge/resource_analyzer_node.py +378 -0
  61. kailash/nodes/edge/resource_optimizer_node.py +501 -0
  62. kailash/nodes/edge/resource_scaler_node.py +397 -0
  63. kailash/nodes/governance.py +410 -0
  64. kailash/nodes/ports.py +676 -0
  65. kailash/nodes/rag/registry.py +1 -1
  66. kailash/nodes/transaction/distributed_transaction_manager.py +48 -1
  67. kailash/nodes/transaction/saga_state_storage.py +2 -1
  68. kailash/nodes/validation.py +8 -8
  69. kailash/runtime/local.py +374 -1
  70. kailash/runtime/validation/__init__.py +12 -0
  71. kailash/runtime/validation/connection_context.py +119 -0
  72. kailash/runtime/validation/enhanced_error_formatter.py +202 -0
  73. kailash/runtime/validation/error_categorizer.py +164 -0
  74. kailash/runtime/validation/import_validator.py +446 -0
  75. kailash/runtime/validation/metrics.py +380 -0
  76. kailash/runtime/validation/performance.py +615 -0
  77. kailash/runtime/validation/suggestion_engine.py +212 -0
  78. kailash/testing/fixtures.py +2 -2
  79. kailash/utils/data_paths.py +74 -0
  80. kailash/workflow/builder.py +413 -8
  81. kailash/workflow/contracts.py +418 -0
  82. kailash/workflow/edge_infrastructure.py +369 -0
  83. kailash/workflow/mermaid_visualizer.py +3 -1
  84. kailash/workflow/migration.py +3 -3
  85. kailash/workflow/templates.py +6 -6
  86. kailash/workflow/type_inference.py +669 -0
  87. kailash/workflow/validation.py +134 -3
  88. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/METADATA +52 -34
  89. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/RECORD +93 -42
  90. kailash/nexus/__init__.py +0 -21
  91. kailash/nexus/cli/__init__.py +0 -5
  92. kailash/nexus/cli/__main__.py +0 -6
  93. kailash/nexus/cli/main.py +0 -176
  94. kailash/nexus/factory.py +0 -413
  95. kailash/nexus/gateway.py +0 -545
  96. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/WHEEL +0 -0
  97. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/entry_points.txt +0 -0
  98. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/licenses/LICENSE +0 -0
  99. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/top_level.txt +0 -0
@@ -902,7 +902,54 @@ class DistributedTransactionManagerNode(AsyncNode):
902
902
  from .saga_state_storage import InMemoryStateStorage
903
903
 
904
904
  return InMemoryStateStorage()
905
- return DatabaseStateStorage(
905
+
906
+ # Create a DTM-specific storage wrapper that uses transaction_id instead of saga_id
907
+ class DTMDatabaseStorage:
908
+ def __init__(self, db_pool, table_name):
909
+ self.db_pool = db_pool
910
+ self.table_name = table_name
911
+
912
+ async def save_state(self, transaction_id: str, state_data: dict):
913
+ """Save DTM state using transaction_id."""
914
+ import json
915
+ from datetime import UTC, datetime
916
+
917
+ async with self.db_pool.acquire() as conn:
918
+ query = f"""
919
+ INSERT INTO {self.table_name}
920
+ (transaction_id, transaction_name, status, state_data, updated_at)
921
+ VALUES ($1, $2, $3, $4, $5)
922
+ ON CONFLICT (transaction_id)
923
+ DO UPDATE SET
924
+ transaction_name = EXCLUDED.transaction_name,
925
+ status = EXCLUDED.status,
926
+ state_data = EXCLUDED.state_data,
927
+ updated_at = EXCLUDED.updated_at
928
+ """
929
+
930
+ await conn.execute(
931
+ query,
932
+ transaction_id,
933
+ state_data.get("transaction_name", ""),
934
+ state_data.get("status", ""),
935
+ json.dumps(state_data),
936
+ datetime.now(UTC),
937
+ )
938
+
939
+ async def load_state(self, transaction_id: str):
940
+ """Load DTM state using transaction_id."""
941
+ async with self.db_pool.acquire() as conn:
942
+ row = await conn.fetchrow(
943
+ f"SELECT state_data FROM {self.table_name} WHERE transaction_id = $1",
944
+ transaction_id,
945
+ )
946
+ if row:
947
+ import json
948
+
949
+ return json.loads(row["state_data"])
950
+ return None
951
+
952
+ return DTMDatabaseStorage(
906
953
  db_pool,
907
954
  self.storage_config.get("table_name", "distributed_transaction_states"),
908
955
  )
@@ -405,7 +405,8 @@ class StorageFactory:
405
405
  if not db_pool:
406
406
  raise ValueError("db_pool is required for database storage")
407
407
  return DatabaseStateStorage(
408
- db_pool, kwargs.get("table_name", "saga_states")
408
+ db_pool,
409
+ kwargs.get("saga_table_name", kwargs.get("table_name", "saga_states")),
409
410
  )
410
411
  else:
411
412
  raise ValueError(f"Unknown storage type: {storage_type}")
@@ -31,13 +31,13 @@ class NodeValidator:
31
31
  r"return\s+(?!.*\{.*result.*\})": ValidationSuggestion(
32
32
  message="PythonCodeNode must return data wrapped in {'result': ...}",
33
33
  code_example='return {"result": your_data} # Not: return your_data',
34
- doc_link="sdk-users/developer/07-troubleshooting.md#pythoncodenode-output",
34
+ doc_link="sdk-users/3-development/guides/troubleshooting.md#pythoncodenode-output",
35
35
  ),
36
36
  # File path mistakes
37
37
  r"^(?!/).*\.(csv|json|txt)$": ValidationSuggestion(
38
38
  message="File paths should be absolute, not relative",
39
39
  code_example='file_path="/data/inputs/file.csv" # Not: file_path="file.csv"',
40
- doc_link="sdk-users/developer/QUICK_REFERENCE.md#file-paths",
40
+ doc_link="sdk-users/3-development/quick-reference.md#file-paths",
41
41
  ),
42
42
  # Node naming mistakes
43
43
  r"Node$": ValidationSuggestion(
@@ -51,13 +51,13 @@ class NodeValidator:
51
51
  r"f['\"].*SELECT.*\{": ValidationSuggestion(
52
52
  message="Avoid f-strings in SQL queries - use parameterized queries",
53
53
  code_example='query="SELECT * FROM users WHERE id = %s", params=[user_id]',
54
- doc_link="sdk-users/security/sql-best-practices.md",
54
+ doc_link="sdk-users/5-enterprise/security-patterns.md#sql-best-practices",
55
55
  ),
56
56
  # Missing required fields
57
57
  r"TypeError.*missing.*required": ValidationSuggestion(
58
58
  message="Required parameter missing",
59
59
  code_example="Check node documentation for required parameters",
60
- doc_link="sdk-users/nodes/comprehensive-node-catalog.md",
60
+ doc_link="sdk-users/6-reference/nodes/comprehensive-node-catalog.md",
61
61
  ),
62
62
  }
63
63
 
@@ -143,7 +143,7 @@ class NodeValidator:
143
143
  ValidationSuggestion(
144
144
  message=f"Parameter '{param_name}' expects {expected_type.__name__}, got {type(value).__name__}",
145
145
  code_example=f"{param_name}={cls._get_type_example(expected_type)}",
146
- doc_link=f"sdk-users/nodes/{node.__class__.__name__.lower()}.md",
146
+ doc_link=f"sdk-users/6-reference/nodes/{node.__class__.__name__.lower()}.md",
147
147
  )
148
148
  )
149
149
  except Exception:
@@ -272,9 +272,9 @@ PythonCodeNode.from_function("processor", process)
272
272
  [
273
273
  "",
274
274
  "🔗 Resources:",
275
- " - Node Catalog: sdk-users/nodes/comprehensive-node-catalog.md",
276
- " - Quick Reference: sdk-users/developer/QUICK_REFERENCE.md",
277
- " - Troubleshooting: sdk-users/developer/07-troubleshooting.md",
275
+ " - Node Catalog: sdk-users/6-reference/nodes/comprehensive-node-catalog.md",
276
+ " - Quick Reference: sdk-users/3-development/quick-reference.md",
277
+ " - Troubleshooting: sdk-users/3-development/guides/troubleshooting.md",
278
278
  ]
279
279
  )
280
280
 
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)
@@ -313,6 +336,17 @@ class LocalRuntime:
313
336
  if self.enable_security and self.user_context:
314
337
  self._check_workflow_access(workflow)
315
338
 
339
+ # Extract workflow context BEFORE parameter processing
340
+ # This prevents workflow_context from being treated as a workflow-level parameter
341
+ workflow_context = {}
342
+ if parameters and "workflow_context" in parameters:
343
+ workflow_context = parameters.pop("workflow_context")
344
+ if not isinstance(workflow_context, dict):
345
+ workflow_context = {}
346
+
347
+ # Store workflow context for inspection/cleanup
348
+ self._current_workflow_context = workflow_context
349
+
316
350
  # Transform workflow-level parameters if needed
317
351
  processed_parameters = self._process_workflow_parameters(
318
352
  workflow, parameters
@@ -381,6 +415,7 @@ class LocalRuntime:
381
415
  task_manager=task_manager,
382
416
  run_id=run_id,
383
417
  parameters=processed_parameters or {},
418
+ workflow_context=workflow_context,
384
419
  )
385
420
 
386
421
  # Enterprise Audit: Log successful completion
@@ -403,6 +438,16 @@ class LocalRuntime:
403
438
  except Exception as e:
404
439
  self.logger.warning(f"Failed to update run status: {e}")
405
440
 
441
+ # Final cleanup of all node instances
442
+ for node_id, node_instance in workflow._node_instances.items():
443
+ if hasattr(node_instance, "cleanup"):
444
+ try:
445
+ await node_instance.cleanup()
446
+ except Exception as cleanup_error:
447
+ self.logger.warning(
448
+ f"Error during final cleanup of node {node_id}: {cleanup_error}"
449
+ )
450
+
406
451
  return results, run_id
407
452
 
408
453
  except WorkflowValidationError:
@@ -470,6 +515,7 @@ class LocalRuntime:
470
515
  task_manager: TaskManager | None,
471
516
  run_id: str | None,
472
517
  parameters: dict[str, dict[str, Any]],
518
+ workflow_context: dict[str, Any] | None = None,
473
519
  ) -> dict[str, Any]:
474
520
  """Execute the workflow nodes in topological order.
475
521
 
@@ -499,6 +545,13 @@ class LocalRuntime:
499
545
  node_outputs = {}
500
546
  failed_nodes = []
501
547
 
548
+ # Use the workflow context passed from _execute_async
549
+ if workflow_context is None:
550
+ workflow_context = {}
551
+
552
+ # Store the workflow context for cleanup later
553
+ self._current_workflow_context = workflow_context
554
+
502
555
  # Execute each node
503
556
  for node_id in execution_order:
504
557
  self.logger.info(f"Executing node: {node_id}")
@@ -594,6 +647,13 @@ class LocalRuntime:
594
647
  node_id, inputs
595
648
  )
596
649
 
650
+ # Set workflow context on the node instance
651
+ if hasattr(node_instance, "_workflow_context"):
652
+ node_instance._workflow_context = workflow_context
653
+ else:
654
+ # Initialize the workflow context if it doesn't exist
655
+ node_instance._workflow_context = workflow_context
656
+
597
657
  if self.enable_async and hasattr(node_instance, "execute_async"):
598
658
  # Use async execution method that includes validation
599
659
  outputs = await node_instance.execute_async(**validated_inputs)
@@ -633,6 +693,15 @@ class LocalRuntime:
633
693
  f"Node {node_id} completed successfully in {performance_metrics.duration:.3f}s"
634
694
  )
635
695
 
696
+ # Clean up async resources if the node has a cleanup method
697
+ if hasattr(node_instance, "cleanup"):
698
+ try:
699
+ await node_instance.cleanup()
700
+ except Exception as cleanup_error:
701
+ self.logger.warning(
702
+ f"Error during node {node_id} cleanup: {cleanup_error}"
703
+ )
704
+
636
705
  except Exception as e:
637
706
  failed_nodes.append(node_id)
638
707
  self.logger.error(f"Node {node_id} failed: {e}", exc_info=self.debug)
@@ -646,6 +715,15 @@ class LocalRuntime:
646
715
  ended_at=datetime.now(UTC),
647
716
  )
648
717
 
718
+ # Clean up async resources even on failure
719
+ if hasattr(node_instance, "cleanup"):
720
+ try:
721
+ await node_instance.cleanup()
722
+ except Exception as cleanup_error:
723
+ self.logger.warning(
724
+ f"Error during node {node_id} cleanup after failure: {cleanup_error}"
725
+ )
726
+
649
727
  # Determine if we should continue
650
728
  if self._should_stop_on_error(workflow, node_id):
651
729
  error_msg = f"Node '{node_id}' failed: {e}"
@@ -661,6 +739,9 @@ class LocalRuntime:
661
739
  "failed": True,
662
740
  }
663
741
 
742
+ # Clean up workflow context
743
+ self._current_workflow_context = None
744
+
664
745
  return results
665
746
 
666
747
  def _prepare_node_inputs(
@@ -816,8 +897,232 @@ class LocalRuntime:
816
897
  # Apply parameter overrides
817
898
  inputs.update(parameters)
818
899
 
900
+ # Connection parameter validation (TODO-121) with enhanced error messages and metrics
901
+ if self.connection_validation != "off":
902
+ metrics_collector = get_metrics_collector()
903
+ node_type = type(node_instance).__name__
904
+
905
+ # Start metrics collection
906
+ metrics_collector.start_validation(
907
+ node_id, node_type, self.connection_validation
908
+ )
909
+
910
+ try:
911
+ # Phase 2: Contract validation (if contracts exist in workflow metadata)
912
+ contract_violations = self._validate_connection_contracts(
913
+ workflow, node_id, inputs, node_outputs
914
+ )
915
+
916
+ if contract_violations:
917
+ contract_error_msg = "\n".join(
918
+ [
919
+ f"Contract '{violation['contract']}' violation on connection {violation['connection']}: {violation['error']}"
920
+ for violation in contract_violations
921
+ ]
922
+ )
923
+ raise WorkflowExecutionError(
924
+ f"Connection contract validation failed for node '{node_id}': {contract_error_msg}"
925
+ )
926
+
927
+ # Merge node config with inputs before validation (matches node.execute behavior)
928
+ # This ensures connection validation considers both runtime inputs AND node configuration
929
+ merged_inputs = {**node_instance.config, **inputs}
930
+
931
+ # Handle nested config case (same as in node.execute)
932
+ if "config" in merged_inputs and isinstance(
933
+ merged_inputs["config"], dict
934
+ ):
935
+ nested_config = merged_inputs["config"]
936
+ for key, value in nested_config.items():
937
+ if key not in inputs: # Runtime inputs take precedence
938
+ merged_inputs[key] = value
939
+
940
+ # Use the node's existing validate_inputs method with merged inputs
941
+ validated_inputs = node_instance.validate_inputs(**merged_inputs)
942
+
943
+ # Extract only the runtime inputs from validated results
944
+ # (exclude config parameters that were merged for validation)
945
+ validated_runtime_inputs = {}
946
+ for key, value in validated_inputs.items():
947
+ # Include if it was in original inputs OR not in node config
948
+ # This preserves validated/converted values from runtime inputs
949
+ if key in inputs or key not in node_instance.config:
950
+ validated_runtime_inputs[key] = value
951
+
952
+ # Record successful validation
953
+ metrics_collector.end_validation(node_id, node_type, success=True)
954
+
955
+ # Replace inputs with validated runtime inputs only
956
+ inputs = validated_runtime_inputs
957
+
958
+ except Exception as e:
959
+ # Categorize the error for metrics
960
+ categorizer = ErrorCategorizer()
961
+ error_category = categorizer.categorize_error(e, node_type)
962
+
963
+ # Build connection info for metrics
964
+ connection_info = {"source": "unknown", "target": node_id}
965
+ for connection in workflow.connections:
966
+ if connection.target_node == node_id:
967
+ connection_info["source"] = connection.source_node
968
+ break
969
+
970
+ # Record failed validation
971
+ metrics_collector.end_validation(
972
+ node_id,
973
+ node_type,
974
+ success=False,
975
+ error_category=error_category,
976
+ connection_info=connection_info,
977
+ )
978
+
979
+ # Check for security violations
980
+ if error_category.value == "security_violation":
981
+ metrics_collector.record_security_violation(
982
+ node_id,
983
+ node_type,
984
+ {"message": str(e), "category": "connection_validation"},
985
+ connection_info,
986
+ )
987
+
988
+ # Generate enhanced error message with connection tracing
989
+ error_msg = self._generate_enhanced_validation_error(
990
+ node_id, node_instance, e, workflow, parameters
991
+ )
992
+
993
+ if self.connection_validation == "strict":
994
+ # Strict mode: raise the error with enhanced message
995
+ raise WorkflowExecutionError(error_msg) from e
996
+ elif self.connection_validation == "warn":
997
+ # Warn mode: log enhanced warning and continue with unvalidated inputs
998
+ self.logger.warning(error_msg)
999
+ # Continue with original inputs
1000
+ else:
1001
+ # Record mode bypass for metrics
1002
+ metrics_collector = get_metrics_collector()
1003
+ metrics_collector.record_mode_bypass(
1004
+ node_id, type(node_instance).__name__, self.connection_validation
1005
+ )
1006
+
819
1007
  return inputs
820
1008
 
1009
+ def _generate_enhanced_validation_error(
1010
+ self,
1011
+ node_id: str,
1012
+ node_instance: Node,
1013
+ original_error: Exception,
1014
+ workflow: "Workflow", # Type annotation as string to avoid circular import
1015
+ parameters: dict,
1016
+ ) -> str:
1017
+ """Generate enhanced validation error message with connection tracing and suggestions.
1018
+
1019
+ Args:
1020
+ node_id: ID of the target node that failed validation
1021
+ node_instance: The node instance that failed
1022
+ original_error: Original validation exception
1023
+ workflow: The workflow being executed
1024
+ parameters: Runtime parameters
1025
+
1026
+ Returns:
1027
+ Enhanced error message with connection context and actionable suggestions
1028
+ """
1029
+ # Initialize error enhancement components
1030
+ categorizer = ErrorCategorizer()
1031
+ suggestion_engine = ValidationSuggestionEngine()
1032
+ formatter = EnhancedErrorFormatter()
1033
+
1034
+ # Categorize the error
1035
+ node_type = type(node_instance).__name__
1036
+ error_category = categorizer.categorize_error(original_error, node_type)
1037
+
1038
+ # Build connection context by finding the connections that feed into this node
1039
+ connection_context = self._build_connection_context(
1040
+ node_id, workflow, parameters
1041
+ )
1042
+
1043
+ # Generate suggestion for fixing the error
1044
+ suggestion = suggestion_engine.generate_suggestion(
1045
+ error_category, node_type, connection_context, str(original_error)
1046
+ )
1047
+
1048
+ # Format the enhanced error message
1049
+ if error_category.value == "security_violation":
1050
+ enhanced_msg = formatter.format_security_error(
1051
+ str(original_error), connection_context, suggestion
1052
+ )
1053
+ else:
1054
+ enhanced_msg = formatter.format_enhanced_error(
1055
+ str(original_error), error_category, connection_context, suggestion
1056
+ )
1057
+
1058
+ return enhanced_msg
1059
+
1060
+ def _build_connection_context(
1061
+ self, target_node_id: str, workflow: "Workflow", parameters: dict
1062
+ ) -> ConnectionContext:
1063
+ """Build connection context for error message enhancement.
1064
+
1065
+ Args:
1066
+ target_node_id: ID of the target node
1067
+ workflow: The workflow being executed
1068
+ parameters: Runtime parameters
1069
+
1070
+ Returns:
1071
+ ConnectionContext with source/target information
1072
+ """
1073
+ # Find the primary connection feeding into this node
1074
+ source_node = "unknown"
1075
+ source_port = None
1076
+ target_port = "input"
1077
+ parameter_value = None
1078
+
1079
+ # Look through workflow connections to find what feeds this node
1080
+ for connection in workflow.connections:
1081
+ if connection.target_node == target_node_id:
1082
+ source_node = connection.source_node
1083
+ source_port = connection.source_output
1084
+ target_port = connection.target_input
1085
+
1086
+ # Try to get the actual parameter value from runtime parameters
1087
+ if target_port in parameters:
1088
+ parameter_value = parameters[target_port]
1089
+ break
1090
+
1091
+ # If no connection found, this might be a direct parameter issue
1092
+ if source_node == "unknown" and parameters:
1093
+ # Find the first parameter that might have caused the issue
1094
+ for key, value in parameters.items():
1095
+ parameter_value = value
1096
+ target_port = key
1097
+ break
1098
+
1099
+ return ConnectionContext(
1100
+ source_node=source_node,
1101
+ source_port=source_port,
1102
+ target_node=target_node_id,
1103
+ target_port=target_port,
1104
+ parameter_value=parameter_value,
1105
+ validation_mode=self.connection_validation,
1106
+ )
1107
+
1108
+ def get_validation_metrics(self) -> Dict[str, Any]:
1109
+ """Get validation performance metrics for the runtime.
1110
+
1111
+ Returns:
1112
+ Dictionary containing performance and security metrics
1113
+ """
1114
+ metrics_collector = get_metrics_collector()
1115
+ return {
1116
+ "performance_summary": metrics_collector.get_performance_summary(),
1117
+ "security_report": metrics_collector.get_security_report(),
1118
+ "raw_metrics": metrics_collector.export_metrics() if self.debug else None,
1119
+ }
1120
+
1121
+ def reset_validation_metrics(self) -> None:
1122
+ """Reset validation metrics collector."""
1123
+ metrics_collector = get_metrics_collector()
1124
+ metrics_collector.reset_metrics()
1125
+
821
1126
  def _should_stop_on_error(self, workflow: Workflow, node_id: str) -> bool:
822
1127
  """Determine if execution should stop when a node fails.
823
1128
 
@@ -1200,3 +1505,71 @@ class LocalRuntime:
1200
1505
 
1201
1506
  # Default to workflow-level format
1202
1507
  return False
1508
+
1509
+ def _validate_connection_contracts(
1510
+ self,
1511
+ workflow: Workflow,
1512
+ target_node_id: str,
1513
+ target_inputs: dict[str, Any],
1514
+ node_outputs: dict[str, dict[str, Any]],
1515
+ ) -> list[dict[str, str]]:
1516
+ """
1517
+ Validate connection contracts for a target node.
1518
+
1519
+ Args:
1520
+ workflow: The workflow being executed
1521
+ target_node_id: ID of the target node
1522
+ target_inputs: Inputs being passed to the target node
1523
+ node_outputs: Outputs from all previously executed nodes
1524
+
1525
+ Returns:
1526
+ List of contract violations (empty if all valid)
1527
+ """
1528
+ violations = []
1529
+
1530
+ # Get connection contracts from workflow metadata
1531
+ connection_contracts = workflow.metadata.get("connection_contracts", {})
1532
+ if not connection_contracts:
1533
+ return violations # No contracts to validate
1534
+
1535
+ # Create contract validator
1536
+ validator = ContractValidator()
1537
+
1538
+ # Find all connections targeting this node
1539
+ for connection in workflow.connections:
1540
+ if connection.target_node == target_node_id:
1541
+ connection_id = f"{connection.source_node}.{connection.source_output} → {connection.target_node}.{connection.target_input}"
1542
+
1543
+ # Check if this connection has a contract
1544
+ if connection_id in connection_contracts:
1545
+ contract_dict = connection_contracts[connection_id]
1546
+
1547
+ # Reconstruct contract from dictionary
1548
+ contract = ConnectionContract.from_dict(contract_dict)
1549
+
1550
+ # Get source data from node outputs
1551
+ source_data = None
1552
+ if connection.source_node in node_outputs:
1553
+ source_outputs = node_outputs[connection.source_node]
1554
+ if connection.source_output in source_outputs:
1555
+ source_data = source_outputs[connection.source_output]
1556
+
1557
+ # Get target data from inputs
1558
+ target_data = target_inputs.get(connection.target_input)
1559
+
1560
+ # Validate the connection if we have data
1561
+ if source_data is not None or target_data is not None:
1562
+ is_valid, errors = validator.validate_connection(
1563
+ contract, source_data, target_data
1564
+ )
1565
+
1566
+ if not is_valid:
1567
+ violations.append(
1568
+ {
1569
+ "connection": connection_id,
1570
+ "contract": contract.name,
1571
+ "error": "; ".join(errors),
1572
+ }
1573
+ )
1574
+
1575
+ return violations
@@ -0,0 +1,12 @@
1
+ """
2
+ Runtime validation utilities for the Kailash SDK.
3
+
4
+ This module provides validation tools for ensuring production-ready code:
5
+ - Import path validation for deployment compatibility
6
+ - Parameter validation for workflow execution
7
+ - Security validation for enterprise deployments
8
+ """
9
+
10
+ from .import_validator import ImportIssue, ImportIssueType, ImportPathValidator
11
+
12
+ __all__ = ["ImportPathValidator", "ImportIssue", "ImportIssueType"]