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
@@ -0,0 +1,418 @@
|
|
1
|
+
"""
|
2
|
+
Connection Contract System for Kailash Workflows
|
3
|
+
|
4
|
+
Provides contract-based validation for workflow connections using JSON Schema.
|
5
|
+
Contracts define the expected data format, constraints, and security policies
|
6
|
+
for data flowing between nodes.
|
7
|
+
|
8
|
+
Design Goals:
|
9
|
+
1. Declarative validation using JSON Schema
|
10
|
+
2. Reusable contract definitions
|
11
|
+
3. Security policy enforcement
|
12
|
+
4. Backward compatibility with existing workflows
|
13
|
+
5. Clear contract violation messages
|
14
|
+
"""
|
15
|
+
|
16
|
+
import json
|
17
|
+
from dataclasses import dataclass, field
|
18
|
+
from enum import Enum
|
19
|
+
from typing import Any, Dict, List, Optional, Union
|
20
|
+
|
21
|
+
import jsonschema
|
22
|
+
from jsonschema import Draft7Validator
|
23
|
+
from jsonschema import ValidationError as JsonSchemaError
|
24
|
+
|
25
|
+
from kailash.sdk_exceptions import WorkflowValidationError
|
26
|
+
|
27
|
+
|
28
|
+
class SecurityPolicy(Enum):
|
29
|
+
"""Security policies that can be applied to connections."""
|
30
|
+
|
31
|
+
NONE = "none"
|
32
|
+
"""No special security policy"""
|
33
|
+
|
34
|
+
NO_PII = "no_pii"
|
35
|
+
"""No personally identifiable information allowed"""
|
36
|
+
|
37
|
+
NO_CREDENTIALS = "no_credentials"
|
38
|
+
"""No passwords, tokens, or keys allowed"""
|
39
|
+
|
40
|
+
NO_SQL = "no_sql"
|
41
|
+
"""No SQL queries allowed (prevents injection)"""
|
42
|
+
|
43
|
+
SANITIZED = "sanitized"
|
44
|
+
"""Data must be sanitized/escaped"""
|
45
|
+
|
46
|
+
ENCRYPTED = "encrypted"
|
47
|
+
"""Data must be encrypted in transit"""
|
48
|
+
|
49
|
+
|
50
|
+
@dataclass
|
51
|
+
class ConnectionContract:
|
52
|
+
"""
|
53
|
+
Defines a contract for data flowing through a connection.
|
54
|
+
|
55
|
+
A contract specifies:
|
56
|
+
- JSON Schema for data validation
|
57
|
+
- Security policies to enforce
|
58
|
+
- Transformation rules (optional)
|
59
|
+
- Audit requirements
|
60
|
+
"""
|
61
|
+
|
62
|
+
name: str
|
63
|
+
"""Human-readable name for the contract"""
|
64
|
+
|
65
|
+
description: str = ""
|
66
|
+
"""Description of what this contract validates"""
|
67
|
+
|
68
|
+
source_schema: Optional[Dict[str, Any]] = None
|
69
|
+
"""JSON Schema for validating source node output"""
|
70
|
+
|
71
|
+
target_schema: Optional[Dict[str, Any]] = None
|
72
|
+
"""JSON Schema for validating target node input"""
|
73
|
+
|
74
|
+
security_policies: List[SecurityPolicy] = field(default_factory=list)
|
75
|
+
"""Security policies to enforce on this connection"""
|
76
|
+
|
77
|
+
transformations: Optional[Dict[str, Any]] = None
|
78
|
+
"""Optional transformations to apply (e.g., type coercion rules)"""
|
79
|
+
|
80
|
+
audit_level: str = "normal"
|
81
|
+
"""Audit level: 'none', 'normal', 'detailed'"""
|
82
|
+
|
83
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
84
|
+
"""Additional metadata for the contract"""
|
85
|
+
|
86
|
+
def __post_init__(self):
|
87
|
+
"""Validate contract after initialization."""
|
88
|
+
# Validate schemas if provided
|
89
|
+
if self.source_schema:
|
90
|
+
try:
|
91
|
+
Draft7Validator.check_schema(self.source_schema)
|
92
|
+
except Exception as e:
|
93
|
+
raise ValueError(f"Invalid source schema: {e}")
|
94
|
+
|
95
|
+
if self.target_schema:
|
96
|
+
try:
|
97
|
+
Draft7Validator.check_schema(self.target_schema)
|
98
|
+
except Exception as e:
|
99
|
+
raise ValueError(f"Invalid target schema: {e}")
|
100
|
+
|
101
|
+
def validate_source(self, data: Any) -> tuple[bool, Optional[str]]:
|
102
|
+
"""
|
103
|
+
Validate data against source schema.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
data: Data to validate
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
Tuple of (is_valid, error_message)
|
110
|
+
"""
|
111
|
+
if not self.source_schema:
|
112
|
+
return True, None
|
113
|
+
|
114
|
+
try:
|
115
|
+
validator = Draft7Validator(self.source_schema)
|
116
|
+
validator.validate(data)
|
117
|
+
return True, None
|
118
|
+
except JsonSchemaError as e:
|
119
|
+
return False, f"Source validation failed: {e.message}"
|
120
|
+
|
121
|
+
def validate_target(self, data: Any) -> tuple[bool, Optional[str]]:
|
122
|
+
"""
|
123
|
+
Validate data against target schema.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
data: Data to validate
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
Tuple of (is_valid, error_message)
|
130
|
+
"""
|
131
|
+
if not self.target_schema:
|
132
|
+
return True, None
|
133
|
+
|
134
|
+
try:
|
135
|
+
validator = Draft7Validator(self.target_schema)
|
136
|
+
validator.validate(data)
|
137
|
+
return True, None
|
138
|
+
except JsonSchemaError as e:
|
139
|
+
return False, f"Target validation failed: {e.message}"
|
140
|
+
|
141
|
+
def check_security_policies(self, data: Any) -> tuple[bool, Optional[str]]:
|
142
|
+
"""
|
143
|
+
Check if data complies with security policies.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
data: Data to check
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
Tuple of (is_compliant, violation_message)
|
150
|
+
"""
|
151
|
+
data_str = str(data).lower() if data is not None else ""
|
152
|
+
|
153
|
+
for policy in self.security_policies:
|
154
|
+
if policy == SecurityPolicy.NO_PII:
|
155
|
+
# Simple PII detection (should be more sophisticated in production)
|
156
|
+
pii_patterns = ["ssn", "social security", "credit card", "passport"]
|
157
|
+
for pattern in pii_patterns:
|
158
|
+
if pattern in data_str:
|
159
|
+
return False, f"PII detected: {pattern}"
|
160
|
+
|
161
|
+
elif policy == SecurityPolicy.NO_CREDENTIALS:
|
162
|
+
cred_patterns = [
|
163
|
+
"password",
|
164
|
+
"token",
|
165
|
+
"api_key",
|
166
|
+
"secret",
|
167
|
+
"private_key",
|
168
|
+
]
|
169
|
+
for pattern in cred_patterns:
|
170
|
+
if pattern in data_str:
|
171
|
+
return False, f"Credential detected: {pattern}"
|
172
|
+
|
173
|
+
elif policy == SecurityPolicy.NO_SQL:
|
174
|
+
sql_patterns = [
|
175
|
+
"select ",
|
176
|
+
"insert ",
|
177
|
+
"update ",
|
178
|
+
"delete ",
|
179
|
+
"drop ",
|
180
|
+
"union ",
|
181
|
+
]
|
182
|
+
for pattern in sql_patterns:
|
183
|
+
if pattern in data_str:
|
184
|
+
return False, f"SQL pattern detected: {pattern}"
|
185
|
+
|
186
|
+
return True, None
|
187
|
+
|
188
|
+
def to_dict(self) -> Dict[str, Any]:
|
189
|
+
"""Convert contract to dictionary for serialization."""
|
190
|
+
return {
|
191
|
+
"name": self.name,
|
192
|
+
"description": self.description,
|
193
|
+
"source_schema": self.source_schema,
|
194
|
+
"target_schema": self.target_schema,
|
195
|
+
"security_policies": [p.value for p in self.security_policies],
|
196
|
+
"transformations": self.transformations,
|
197
|
+
"audit_level": self.audit_level,
|
198
|
+
"metadata": self.metadata,
|
199
|
+
}
|
200
|
+
|
201
|
+
@classmethod
|
202
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ConnectionContract":
|
203
|
+
"""Create contract from dictionary."""
|
204
|
+
return cls(
|
205
|
+
name=data["name"],
|
206
|
+
description=data.get("description", ""),
|
207
|
+
source_schema=data.get("source_schema"),
|
208
|
+
target_schema=data.get("target_schema"),
|
209
|
+
security_policies=[
|
210
|
+
SecurityPolicy(p) for p in data.get("security_policies", [])
|
211
|
+
],
|
212
|
+
transformations=data.get("transformations"),
|
213
|
+
audit_level=data.get("audit_level", "normal"),
|
214
|
+
metadata=data.get("metadata", {}),
|
215
|
+
)
|
216
|
+
|
217
|
+
|
218
|
+
class ContractRegistry:
|
219
|
+
"""Registry for reusable connection contracts."""
|
220
|
+
|
221
|
+
def __init__(self):
|
222
|
+
self._contracts: Dict[str, ConnectionContract] = {}
|
223
|
+
self._initialize_common_contracts()
|
224
|
+
|
225
|
+
def _initialize_common_contracts(self):
|
226
|
+
"""Initialize commonly used contracts."""
|
227
|
+
# String data contract
|
228
|
+
self.register(
|
229
|
+
ConnectionContract(
|
230
|
+
name="string_data",
|
231
|
+
description="Basic string data contract",
|
232
|
+
source_schema={"type": "string"},
|
233
|
+
target_schema={"type": "string"},
|
234
|
+
)
|
235
|
+
)
|
236
|
+
|
237
|
+
# Numeric data contract
|
238
|
+
self.register(
|
239
|
+
ConnectionContract(
|
240
|
+
name="numeric_data",
|
241
|
+
description="Numeric data contract",
|
242
|
+
source_schema={"type": "number"},
|
243
|
+
target_schema={"type": "number"},
|
244
|
+
)
|
245
|
+
)
|
246
|
+
|
247
|
+
# File path contract
|
248
|
+
self.register(
|
249
|
+
ConnectionContract(
|
250
|
+
name="file_path",
|
251
|
+
description="File path validation",
|
252
|
+
source_schema={
|
253
|
+
"type": "string",
|
254
|
+
"pattern": "^[^\\0]+$", # No null bytes
|
255
|
+
},
|
256
|
+
target_schema={"type": "string", "pattern": "^[^\\0]+$"},
|
257
|
+
security_policies=[SecurityPolicy.NO_SQL],
|
258
|
+
)
|
259
|
+
)
|
260
|
+
|
261
|
+
# SQL query contract
|
262
|
+
self.register(
|
263
|
+
ConnectionContract(
|
264
|
+
name="sql_query",
|
265
|
+
description="SQL query with injection protection",
|
266
|
+
source_schema={"type": "string"},
|
267
|
+
target_schema={"type": "string"},
|
268
|
+
security_policies=[SecurityPolicy.SANITIZED],
|
269
|
+
)
|
270
|
+
)
|
271
|
+
|
272
|
+
# User data contract
|
273
|
+
self.register(
|
274
|
+
ConnectionContract(
|
275
|
+
name="user_data",
|
276
|
+
description="User data with PII protection",
|
277
|
+
source_schema={
|
278
|
+
"type": "object",
|
279
|
+
"properties": {
|
280
|
+
"id": {"type": "string"},
|
281
|
+
"name": {"type": "string"},
|
282
|
+
"email": {"type": "string", "format": "email"},
|
283
|
+
},
|
284
|
+
"required": ["id"],
|
285
|
+
},
|
286
|
+
target_schema={
|
287
|
+
"type": "object",
|
288
|
+
"properties": {
|
289
|
+
"id": {"type": "string"},
|
290
|
+
"name": {"type": "string"},
|
291
|
+
"email": {"type": "string", "format": "email"},
|
292
|
+
},
|
293
|
+
},
|
294
|
+
security_policies=[SecurityPolicy.NO_CREDENTIALS],
|
295
|
+
audit_level="detailed",
|
296
|
+
)
|
297
|
+
)
|
298
|
+
|
299
|
+
def register(self, contract: ConnectionContract) -> None:
|
300
|
+
"""Register a contract in the registry."""
|
301
|
+
self._contracts[contract.name] = contract
|
302
|
+
|
303
|
+
def get(self, name: str) -> Optional[ConnectionContract]:
|
304
|
+
"""Get a contract by name."""
|
305
|
+
return self._contracts.get(name)
|
306
|
+
|
307
|
+
def list_contracts(self) -> List[str]:
|
308
|
+
"""List all available contract names."""
|
309
|
+
return list(self._contracts.keys())
|
310
|
+
|
311
|
+
def create_from_schema(
|
312
|
+
self,
|
313
|
+
name: str,
|
314
|
+
schema: Dict[str, Any],
|
315
|
+
security_policies: Optional[List[SecurityPolicy]] = None,
|
316
|
+
) -> ConnectionContract:
|
317
|
+
"""Create and register a contract from a JSON schema."""
|
318
|
+
contract = ConnectionContract(
|
319
|
+
name=name,
|
320
|
+
source_schema=schema,
|
321
|
+
target_schema=schema,
|
322
|
+
security_policies=security_policies or [],
|
323
|
+
)
|
324
|
+
self.register(contract)
|
325
|
+
return contract
|
326
|
+
|
327
|
+
|
328
|
+
class ContractValidator:
|
329
|
+
"""Validates data against connection contracts."""
|
330
|
+
|
331
|
+
def __init__(self, registry: Optional[ContractRegistry] = None):
|
332
|
+
self.registry = registry or ContractRegistry()
|
333
|
+
|
334
|
+
def validate_connection(
|
335
|
+
self,
|
336
|
+
contract: Union[str, ConnectionContract],
|
337
|
+
source_data: Any,
|
338
|
+
target_data: Any,
|
339
|
+
) -> tuple[bool, List[str]]:
|
340
|
+
"""
|
341
|
+
Validate a connection against a contract.
|
342
|
+
|
343
|
+
Args:
|
344
|
+
contract: Contract name or instance
|
345
|
+
source_data: Data from source node
|
346
|
+
target_data: Data for target node
|
347
|
+
|
348
|
+
Returns:
|
349
|
+
Tuple of (is_valid, list_of_errors)
|
350
|
+
"""
|
351
|
+
# Resolve contract
|
352
|
+
if isinstance(contract, str):
|
353
|
+
contract_obj = self.registry.get(contract)
|
354
|
+
if not contract_obj:
|
355
|
+
return False, [f"Contract '{contract}' not found"]
|
356
|
+
contract = contract_obj
|
357
|
+
|
358
|
+
errors = []
|
359
|
+
|
360
|
+
# Validate source
|
361
|
+
valid, error = contract.validate_source(source_data)
|
362
|
+
if not valid:
|
363
|
+
errors.append(f"Source validation: {error}")
|
364
|
+
|
365
|
+
# Validate target
|
366
|
+
valid, error = contract.validate_target(target_data)
|
367
|
+
if not valid:
|
368
|
+
errors.append(f"Target validation: {error}")
|
369
|
+
|
370
|
+
# Check security policies on both source and target
|
371
|
+
for data, label in [(source_data, "source"), (target_data, "target")]:
|
372
|
+
compliant, violation = contract.check_security_policies(data)
|
373
|
+
if not compliant:
|
374
|
+
errors.append(f"Security policy violation ({label}): {violation}")
|
375
|
+
|
376
|
+
return len(errors) == 0, errors
|
377
|
+
|
378
|
+
def suggest_contract(self, data: Any) -> Optional[str]:
|
379
|
+
"""
|
380
|
+
Suggest a suitable contract based on data type.
|
381
|
+
|
382
|
+
Args:
|
383
|
+
data: Sample data
|
384
|
+
|
385
|
+
Returns:
|
386
|
+
Suggested contract name or None
|
387
|
+
"""
|
388
|
+
if isinstance(data, str):
|
389
|
+
# Check for specific string patterns
|
390
|
+
if "@" in data and "." in data:
|
391
|
+
return "email_data"
|
392
|
+
elif "/" in data or "\\" in data:
|
393
|
+
return "file_path"
|
394
|
+
elif any(sql in data.lower() for sql in ["select", "insert", "update"]):
|
395
|
+
return "sql_query"
|
396
|
+
else:
|
397
|
+
return "string_data"
|
398
|
+
|
399
|
+
elif isinstance(data, (int, float)):
|
400
|
+
return "numeric_data"
|
401
|
+
|
402
|
+
elif isinstance(data, dict):
|
403
|
+
if "id" in data or "name" in data:
|
404
|
+
return "user_data"
|
405
|
+
|
406
|
+
return None
|
407
|
+
|
408
|
+
|
409
|
+
# Global contract registry instance
|
410
|
+
_global_registry = None
|
411
|
+
|
412
|
+
|
413
|
+
def get_contract_registry() -> ContractRegistry:
|
414
|
+
"""Get the global contract registry."""
|
415
|
+
global _global_registry
|
416
|
+
if _global_registry is None:
|
417
|
+
_global_registry = ContractRegistry()
|
418
|
+
return _global_registry
|