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.
- kailash/__init__.py +5 -11
- kailash/channels/__init__.py +2 -1
- kailash/channels/mcp_channel.py +23 -4
- kailash/cli/__init__.py +11 -1
- kailash/cli/validate_imports.py +202 -0
- kailash/cli/validation_audit.py +570 -0
- kailash/core/actors/supervisor.py +1 -1
- kailash/core/resilience/bulkhead.py +15 -5
- kailash/core/resilience/circuit_breaker.py +74 -1
- kailash/core/resilience/health_monitor.py +433 -33
- 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_migration_service.py +384 -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/mcp_server/server.py +351 -8
- kailash/mcp_server/transports.py +305 -0
- kailash/middleware/gateway/event_store.py +1 -0
- 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 +622 -1
- kailash/nodes/code/python.py +44 -3
- kailash/nodes/data/async_sql.py +42 -20
- 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 +396 -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/governance.py +410 -0
- kailash/nodes/ports.py +676 -0
- kailash/nodes/rag/registry.py +1 -1
- kailash/nodes/transaction/distributed_transaction_manager.py +48 -1
- kailash/nodes/transaction/saga_state_storage.py +2 -1
- kailash/nodes/validation.py +8 -8
- kailash/runtime/local.py +374 -1
- kailash/runtime/validation/__init__.py +12 -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/import_validator.py +446 -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/utils/data_paths.py +74 -0
- kailash/workflow/builder.py +413 -8
- kailash/workflow/contracts.py +418 -0
- kailash/workflow/edge_infrastructure.py +369 -0
- kailash/workflow/mermaid_visualizer.py +3 -1
- kailash/workflow/migration.py +3 -3
- kailash/workflow/templates.py +6 -6
- kailash/workflow/type_inference.py +669 -0
- kailash/workflow/validation.py +134 -3
- {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/METADATA +52 -34
- {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/RECORD +93 -42
- 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.6.dist-info}/WHEEL +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/entry_points.txt +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
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,
|
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}")
|
kailash/nodes/validation.py
CHANGED
@@ -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/
|
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/
|
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
|
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/
|
277
|
-
" - Troubleshooting: sdk-users/
|
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"]
|