kailash 0.1.4__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control.py +740 -0
  3. kailash/api/__main__.py +6 -0
  4. kailash/api/auth.py +668 -0
  5. kailash/api/custom_nodes.py +285 -0
  6. kailash/api/custom_nodes_secure.py +377 -0
  7. kailash/api/database.py +620 -0
  8. kailash/api/studio.py +915 -0
  9. kailash/api/studio_secure.py +893 -0
  10. kailash/mcp/__init__.py +53 -0
  11. kailash/mcp/__main__.py +13 -0
  12. kailash/mcp/ai_registry_server.py +712 -0
  13. kailash/mcp/client.py +447 -0
  14. kailash/mcp/client_new.py +334 -0
  15. kailash/mcp/server.py +293 -0
  16. kailash/mcp/server_new.py +336 -0
  17. kailash/mcp/servers/__init__.py +12 -0
  18. kailash/mcp/servers/ai_registry.py +289 -0
  19. kailash/nodes/__init__.py +4 -2
  20. kailash/nodes/ai/__init__.py +38 -0
  21. kailash/nodes/ai/a2a.py +1790 -0
  22. kailash/nodes/ai/agents.py +116 -2
  23. kailash/nodes/ai/ai_providers.py +206 -8
  24. kailash/nodes/ai/intelligent_agent_orchestrator.py +2108 -0
  25. kailash/nodes/ai/iterative_llm_agent.py +1280 -0
  26. kailash/nodes/ai/llm_agent.py +324 -1
  27. kailash/nodes/ai/self_organizing.py +1623 -0
  28. kailash/nodes/api/http.py +106 -25
  29. kailash/nodes/api/rest.py +116 -21
  30. kailash/nodes/base.py +15 -2
  31. kailash/nodes/base_async.py +45 -0
  32. kailash/nodes/base_cycle_aware.py +374 -0
  33. kailash/nodes/base_with_acl.py +338 -0
  34. kailash/nodes/code/python.py +135 -27
  35. kailash/nodes/data/readers.py +116 -53
  36. kailash/nodes/data/writers.py +16 -6
  37. kailash/nodes/logic/__init__.py +8 -0
  38. kailash/nodes/logic/async_operations.py +48 -9
  39. kailash/nodes/logic/convergence.py +642 -0
  40. kailash/nodes/logic/loop.py +153 -0
  41. kailash/nodes/logic/operations.py +212 -27
  42. kailash/nodes/logic/workflow.py +26 -18
  43. kailash/nodes/mixins/__init__.py +11 -0
  44. kailash/nodes/mixins/mcp.py +228 -0
  45. kailash/nodes/mixins.py +387 -0
  46. kailash/nodes/transform/__init__.py +8 -1
  47. kailash/nodes/transform/processors.py +119 -4
  48. kailash/runtime/__init__.py +2 -1
  49. kailash/runtime/access_controlled.py +458 -0
  50. kailash/runtime/local.py +106 -33
  51. kailash/runtime/parallel_cyclic.py +529 -0
  52. kailash/sdk_exceptions.py +90 -5
  53. kailash/security.py +845 -0
  54. kailash/tracking/manager.py +38 -15
  55. kailash/tracking/models.py +1 -1
  56. kailash/tracking/storage/filesystem.py +30 -2
  57. kailash/utils/__init__.py +8 -0
  58. kailash/workflow/__init__.py +18 -0
  59. kailash/workflow/convergence.py +270 -0
  60. kailash/workflow/cycle_analyzer.py +768 -0
  61. kailash/workflow/cycle_builder.py +573 -0
  62. kailash/workflow/cycle_config.py +709 -0
  63. kailash/workflow/cycle_debugger.py +760 -0
  64. kailash/workflow/cycle_exceptions.py +601 -0
  65. kailash/workflow/cycle_profiler.py +671 -0
  66. kailash/workflow/cycle_state.py +338 -0
  67. kailash/workflow/cyclic_runner.py +985 -0
  68. kailash/workflow/graph.py +500 -39
  69. kailash/workflow/migration.py +768 -0
  70. kailash/workflow/safety.py +365 -0
  71. kailash/workflow/templates.py +744 -0
  72. kailash/workflow/validation.py +693 -0
  73. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/METADATA +446 -13
  74. kailash-0.2.0.dist-info/RECORD +125 -0
  75. kailash/nodes/mcp/__init__.py +0 -11
  76. kailash/nodes/mcp/client.py +0 -554
  77. kailash/nodes/mcp/resource.py +0 -682
  78. kailash/nodes/mcp/server.py +0 -577
  79. kailash-0.1.4.dist-info/RECORD +0 -85
  80. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
  81. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
  82. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
  83. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
@@ -7,8 +7,117 @@ from kailash.nodes.base import Node, NodeParameter, register_node
7
7
 
8
8
 
9
9
  @register_node()
10
- class Filter(Node):
11
- """Filters data based on a condition."""
10
+ class FilterNode(Node):
11
+ """
12
+ Filters data based on configurable conditions and operators.
13
+
14
+ This node provides flexible data filtering capabilities for lists and collections,
15
+ supporting various comparison operators and field-based filtering for structured
16
+ data. It's designed to work seamlessly in data processing pipelines, reducing
17
+ datasets to items that match specific criteria.
18
+
19
+ Design Philosophy:
20
+ The FilterNode embodies the principle of "declarative data selection." Rather
21
+ than writing custom filtering code, users declare their filtering criteria
22
+ through simple configuration. The design supports both simple value filtering
23
+ and complex field-based filtering for dictionaries, making it versatile for
24
+ various data structures.
25
+
26
+ Upstream Dependencies:
27
+ - Data source nodes providing lists to filter
28
+ - Transform nodes producing structured data
29
+ - Aggregation nodes generating collections
30
+ - API nodes returning result sets
31
+ - File readers loading datasets
32
+
33
+ Downstream Consumers:
34
+ - Processing nodes working with filtered subsets
35
+ - Aggregation nodes summarizing filtered data
36
+ - Writer nodes exporting filtered results
37
+ - Visualization nodes displaying subsets
38
+ - Decision nodes based on filter results
39
+
40
+ Configuration:
41
+ The node supports flexible filtering options:
42
+ - Field selection for dictionary filtering
43
+ - Multiple comparison operators
44
+ - Type-aware comparisons
45
+ - Null value handling
46
+ - String contains operations
47
+
48
+ Implementation Details:
49
+ - Handles lists of any type (dicts, primitives, objects)
50
+ - Type coercion for numeric comparisons
51
+ - Null-safe operations
52
+ - String conversion for contains operator
53
+ - Preserves original data structure
54
+ - Zero-copy filtering (returns references)
55
+
56
+ Error Handling:
57
+ - Graceful handling of type mismatches
58
+ - Null value comparison logic
59
+ - Empty data returns empty result
60
+ - Invalid field names return no matches
61
+ - Operator errors fail safely
62
+
63
+ Side Effects:
64
+ - No side effects (pure function)
65
+ - Does not modify input data
66
+ - Returns new filtered list
67
+
68
+ Examples:
69
+ >>> # Filter list of numbers
70
+ >>> filter_node = FilterNode()
71
+ >>> result = filter_node.run(
72
+ ... data=[1, 2, 3, 4, 5],
73
+ ... operator=">",
74
+ ... value=3
75
+ ... )
76
+ >>> assert result["filtered_data"] == [4, 5]
77
+ >>>
78
+ >>> # Filter list of dictionaries by field
79
+ >>> users = [
80
+ ... {"name": "Alice", "age": 30},
81
+ ... {"name": "Bob", "age": 25},
82
+ ... {"name": "Charlie", "age": 35}
83
+ ... ]
84
+ >>> result = filter_node.run(
85
+ ... data=users,
86
+ ... field="age",
87
+ ... operator=">=",
88
+ ... value=30
89
+ ... )
90
+ >>> assert len(result["filtered_data"]) == 2
91
+ >>> assert result["filtered_data"][0]["name"] == "Alice"
92
+ >>>
93
+ >>> # String contains filtering
94
+ >>> items = [
95
+ ... {"title": "Python Programming"},
96
+ ... {"title": "Java Development"},
97
+ ... {"title": "Python for Data Science"}
98
+ ... ]
99
+ >>> result = filter_node.run(
100
+ ... data=items,
101
+ ... field="title",
102
+ ... operator="contains",
103
+ ... value="Python"
104
+ ... )
105
+ >>> assert len(result["filtered_data"]) == 2
106
+ >>>
107
+ >>> # Null value handling
108
+ >>> data_with_nulls = [
109
+ ... {"value": 10},
110
+ ... {"value": None},
111
+ ... {"value": 20}
112
+ ... ]
113
+ >>> result = filter_node.run(
114
+ ... data=data_with_nulls,
115
+ ... field="value",
116
+ ... operator="!=",
117
+ ... value=None
118
+ ... )
119
+ >>> assert len(result["filtered_data"]) == 2
120
+ """
12
121
 
13
122
  def get_parameters(self) -> Dict[str, NodeParameter]:
14
123
  return {
@@ -67,8 +176,10 @@ class Filter(Node):
67
176
  try:
68
177
  # Handle None values - they fail most comparisons
69
178
  if item_value is None:
70
- if operator in ["==", "!="]:
71
- return (operator == "==") == (compare_value is None)
179
+ if operator == "==":
180
+ return compare_value is None
181
+ elif operator == "!=":
182
+ return compare_value is not None
72
183
  else:
73
184
  return False # None fails all other comparisons
74
185
 
@@ -379,3 +490,7 @@ class Sort(Node):
379
490
  sorted_data = sorted(data, reverse=reverse)
380
491
 
381
492
  return {"sorted_data": sorted_data}
493
+
494
+
495
+ # Backward compatibility aliases
496
+ Filter = FilterNode
@@ -1,6 +1,7 @@
1
1
  """Runtime engines for the Kailash SDK."""
2
2
 
3
3
  from kailash.runtime.local import LocalRuntime
4
+ from kailash.runtime.parallel_cyclic import ParallelCyclicRuntime
4
5
  from kailash.runtime.runner import WorkflowRunner
5
6
 
6
- __all__ = ["LocalRuntime", "WorkflowRunner"]
7
+ __all__ = ["LocalRuntime", "ParallelCyclicRuntime", "WorkflowRunner"]
@@ -0,0 +1,458 @@
1
+ """
2
+ Access-Controlled Runtime for Kailash SDK
3
+
4
+ This module provides an access-controlled runtime that wraps the standard runtime
5
+ to add permission checks. The standard runtime remains unchanged, ensuring complete
6
+ backward compatibility.
7
+
8
+ Users who don't need access control continue using LocalRuntime as normal.
9
+ Users who need access control use AccessControlledRuntime instead.
10
+
11
+ Example without access control (existing code):
12
+ >>> from kailash.runtime.local import LocalRuntime
13
+ >>> from kailash.workflow import Workflow
14
+ >>> runtime = LocalRuntime()
15
+ >>> workflow = Workflow(workflow_id="test", name="Test")
16
+ >>> result, run_id = runtime.execute(workflow) # Works exactly as before
17
+
18
+ Example with access control (opt-in):
19
+ >>> from kailash.runtime.access_controlled import AccessControlledRuntime
20
+ >>> from kailash.access_control import UserContext, get_access_control_manager
21
+ >>> user = UserContext(user_id="123", tenant_id="abc", email="user@test.com", roles=["analyst"])
22
+ >>> runtime = AccessControlledRuntime(user_context=user)
23
+ >>> # Access control manager is disabled by default for compatibility
24
+ >>> acm = get_access_control_manager()
25
+ >>> acm.enabled # Should be False by default
26
+ False
27
+ """
28
+
29
+ import logging
30
+ from typing import Any, Dict, List, Optional, Tuple
31
+
32
+ from kailash.access_control import (
33
+ AccessControlManager,
34
+ NodePermission,
35
+ PermissionEffect,
36
+ PermissionRule,
37
+ UserContext,
38
+ WorkflowPermission,
39
+ get_access_control_manager,
40
+ )
41
+ from kailash.nodes.base import Node
42
+ from kailash.runtime.local import LocalRuntime
43
+ from kailash.workflow import Workflow
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ class AccessControlledRuntime:
49
+ """
50
+ Runtime with transparent access control layer.
51
+
52
+ This runtime wraps the standard LocalRuntime and adds access control
53
+ checks without modifying the original runtime or requiring any changes
54
+ to existing nodes or workflows.
55
+
56
+ Design Purpose:
57
+ Provides a drop-in replacement for LocalRuntime that adds security
58
+ without breaking existing workflows. Enables role-based access control,
59
+ data masking, and conditional execution based on user permissions.
60
+
61
+ Upstream Dependencies:
62
+ - AccessControlManager for permission evaluation
63
+ - UserContext from authentication systems
64
+ - LocalRuntime for actual workflow execution
65
+ - PermissionRule definitions from configuration
66
+
67
+ Downstream Consumers:
68
+ - Applications requiring secure workflow execution
69
+ - Multi-tenant systems with user isolation
70
+ - Audit systems for compliance logging
71
+ - Data governance systems for access tracking
72
+
73
+ Usage Patterns:
74
+ - Used as direct replacement for LocalRuntime
75
+ - Configured with user context during initialization
76
+ - Integrates with JWT authentication systems
77
+ - Supports both workflow and node-level permissions
78
+
79
+ Implementation Details:
80
+ Wraps LocalRuntime and intercepts workflow execution to add
81
+ permission checks. Creates access-controlled node wrappers that
82
+ evaluate permissions before execution. Supports data masking,
83
+ conditional routing, and fallback execution.
84
+
85
+ Error Handling:
86
+ - Access denied raises PermissionError with clear messages
87
+ - Missing permissions default to deny for security
88
+ - Configuration errors are logged and treated as disabled
89
+ - Evaluation errors fall back to base runtime behavior
90
+
91
+ Side Effects:
92
+ - Logs all access decisions for audit purposes
93
+ - May redirect execution to alternative nodes
94
+ - Applies data masking to sensitive outputs
95
+ - Caches permission decisions for performance
96
+
97
+ Example:
98
+ >>> from kailash.runtime.access_controlled import AccessControlledRuntime
99
+ >>> from kailash.access_control import UserContext
100
+ >>> from kailash.workflow import Workflow
101
+ >>>
102
+ >>> user = UserContext(user_id="123", tenant_id="abc", email="user@test.com", roles=["analyst"])
103
+ >>> runtime = AccessControlledRuntime(user_context=user)
104
+ >>> # By default, access control is disabled for backward compatibility
105
+ >>> workflow = Workflow(workflow_id="test", name="Test Workflow")
106
+ >>> isinstance(runtime, AccessControlledRuntime)
107
+ True
108
+ """
109
+
110
+ def __init__(
111
+ self, user_context: UserContext, base_runtime: Optional[LocalRuntime] = None
112
+ ):
113
+ """
114
+ Initialize access-controlled runtime.
115
+
116
+ Args:
117
+ user_context: The user context for access control decisions
118
+ base_runtime: The underlying runtime to use (defaults to LocalRuntime)
119
+ """
120
+ self.user_context = user_context
121
+ self.base_runtime = base_runtime or LocalRuntime()
122
+ self.acm = get_access_control_manager()
123
+
124
+ # Track skipped nodes for alternative routing
125
+ self._skipped_nodes: set[str] = set()
126
+ self._node_outputs: Dict[str, Any] = {}
127
+
128
+ def execute(
129
+ self, workflow: Workflow, parameters: Optional[Dict[str, Any]] = None
130
+ ) -> Tuple[Any, str]:
131
+ """
132
+ Execute workflow with access control.
133
+
134
+ This method has the exact same signature as the standard runtime,
135
+ ensuring complete compatibility.
136
+ """
137
+ # Check workflow-level access
138
+ workflow_decision = self.acm.check_workflow_access(
139
+ self.user_context, workflow.workflow_id, WorkflowPermission.EXECUTE
140
+ )
141
+
142
+ if not workflow_decision.allowed:
143
+ raise PermissionError(f"Access denied: {workflow_decision.reason}")
144
+
145
+ # For simplicity, directly execute with the base runtime
146
+ # In a full implementation, we would wrap nodes or intercept execution
147
+ # But for this example, we'll rely on the nodes having access control attributes
148
+ return self.base_runtime.execute(workflow, parameters)
149
+
150
+ def _create_controlled_workflow(self, workflow: Workflow) -> Workflow:
151
+ """
152
+ Create a workflow wrapper that enforces access control.
153
+
154
+ This wrapper intercepts node execution to add permission checks
155
+ without modifying the original workflow.
156
+ """
157
+ # Create a new workflow instance
158
+ controlled = Workflow(
159
+ workflow_id=workflow.workflow_id,
160
+ name=workflow.name,
161
+ description=workflow.description,
162
+ version=workflow.version,
163
+ )
164
+
165
+ # Copy graph structure
166
+ controlled.graph = workflow.graph.copy()
167
+
168
+ # Wrap each node with access control
169
+ for node_id in workflow.graph.nodes:
170
+ node_data = workflow.graph.nodes[node_id]
171
+ original_node = node_data.get("node")
172
+
173
+ if original_node:
174
+ # Create access-controlled wrapper for the node
175
+ wrapped_node = self._create_controlled_node(node_id, original_node)
176
+ controlled.graph.nodes[node_id]["node"] = wrapped_node
177
+
178
+ return controlled
179
+
180
+ def _create_controlled_node(self, node_id: str, original_node: Node) -> Node:
181
+ """
182
+ Create an access-controlled wrapper for a node.
183
+
184
+ This wrapper intercepts the node's run() method to add permission
185
+ checks without modifying the original node.
186
+ """
187
+ runtime = self # Capture runtime reference
188
+
189
+ class AccessControlledNodeWrapper(Node):
190
+ """Dynamic wrapper that adds access control to any node"""
191
+
192
+ def __init__(self):
193
+ # Don't initialize Node base class, just store reference
194
+ self._original_node = original_node
195
+ self._node_id = node_id
196
+ # Copy all attributes from original node
197
+ for attr, value in original_node.__dict__.items():
198
+ if not attr.startswith("_"):
199
+ setattr(self, attr, value)
200
+
201
+ def get_parameters(self):
202
+ """Delegate to original node"""
203
+ return self._original_node.get_parameters()
204
+
205
+ def validate_config(self):
206
+ """Delegate to original node if it has the method"""
207
+ if hasattr(self._original_node, "validate_config"):
208
+ return self._original_node.validate_config()
209
+ return True
210
+
211
+ def get_output_schema(self):
212
+ """Delegate to original node"""
213
+ if hasattr(self._original_node, "get_output_schema"):
214
+ return self._original_node.get_output_schema()
215
+ return None
216
+
217
+ def run(self, **inputs) -> Any:
218
+ """Execute with access control checks"""
219
+ # Check execute permission
220
+ execute_decision = runtime.acm.check_node_access(
221
+ runtime.user_context,
222
+ self._node_id,
223
+ NodePermission.EXECUTE,
224
+ runtime_context={"inputs": inputs},
225
+ )
226
+
227
+ if not execute_decision.allowed:
228
+ # Node execution denied
229
+ logger.info(
230
+ f"Node {self._node_id} skipped for user {runtime.user_context.user_id}"
231
+ )
232
+ runtime._skipped_nodes.add(self._node_id)
233
+
234
+ # Check if there's an alternative path
235
+ if execute_decision.redirect_node:
236
+ return {"_redirect_to": execute_decision.redirect_node}
237
+
238
+ # Return empty result
239
+ return {}
240
+
241
+ # Execute the original node
242
+ result = self._original_node.run(**inputs)
243
+
244
+ # Check output read permission
245
+ output_decision = runtime.acm.check_node_access(
246
+ runtime.user_context,
247
+ self._node_id,
248
+ NodePermission.READ_OUTPUT,
249
+ runtime_context={"output": result},
250
+ )
251
+
252
+ if not output_decision.allowed:
253
+ # Mask entire output
254
+ return {"_access_denied": True}
255
+
256
+ # Apply field masking if needed
257
+ if output_decision.masked_fields and isinstance(result, dict):
258
+ result = runtime._mask_fields(result, output_decision.masked_fields)
259
+
260
+ # Store output for conditional routing
261
+ runtime._node_outputs[self._node_id] = result
262
+
263
+ return result
264
+
265
+ # Create instance of wrapper
266
+ wrapper = AccessControlledNodeWrapper()
267
+
268
+ # Preserve node metadata
269
+ wrapper.__class__.__name__ = f"Controlled{original_node.__class__.__name__}"
270
+ wrapper.__class__.__module__ = original_node.__class__.__module__
271
+
272
+ return wrapper
273
+
274
+ @staticmethod
275
+ def _mask_fields(data: Dict[str, Any], fields: List[str]) -> Dict[str, Any]:
276
+ """Mask sensitive fields in data"""
277
+ masked = data.copy()
278
+ for field in fields:
279
+ if field in masked:
280
+ masked[field] = "***MASKED***"
281
+ return masked
282
+
283
+ def _handle_conditional_routing(
284
+ self, node_id: str, true_path: List[str], false_path: List[str]
285
+ ) -> List[str]:
286
+ """
287
+ Determine which path to take based on permissions.
288
+
289
+ This is used for conditional nodes where the path depends on
290
+ user permissions rather than data conditions.
291
+ """
292
+ # Check which path the user has access to
293
+ return self.acm.get_permission_based_route(
294
+ self.user_context, node_id, true_path, false_path
295
+ )
296
+
297
+
298
+ class AccessControlConfig:
299
+ """
300
+ Configuration for access control in workflows.
301
+
302
+ Provides a declarative way to define access rules without modifying
303
+ workflow code. Enables administrators to configure permissions
304
+ externally from workflow definitions.
305
+
306
+ Design Purpose:
307
+ Separates access control policy from workflow implementation,
308
+ enabling dynamic permission changes without code modifications.
309
+ Supports both workflow-level and node-level permission rules.
310
+
311
+ Upstream Dependencies:
312
+ - Administrative interfaces for rule creation
313
+ - Configuration management systems
314
+ - Policy definition templates
315
+
316
+ Downstream Consumers:
317
+ - AccessControlManager for rule application
318
+ - AccessControlledRuntime for secure execution
319
+ - Policy management tools for validation
320
+
321
+ Usage Patterns:
322
+ - Created by administrators or configuration systems
323
+ - Applied to workflows before execution
324
+ - Used for testing different access scenarios
325
+ - Integrated with external policy management
326
+
327
+ Implementation Details:
328
+ Maintains list of PermissionRule objects with helper methods
329
+ for adding common rule types. Rules are applied to manager
330
+ in batch for consistency.
331
+
332
+ Example:
333
+ >>> config = AccessControlConfig()
334
+ >>> config.add_workflow_permission(
335
+ ... workflow_id="analytics",
336
+ ... permission=WorkflowPermission.EXECUTE,
337
+ ... role="analyst"
338
+ ... )
339
+ >>> config.add_node_permission(
340
+ ... workflow_id="analytics",
341
+ ... node_id="sensitive_data",
342
+ ... permission=NodePermission.READ_OUTPUT,
343
+ ... role="admin"
344
+ ... )
345
+ """
346
+
347
+ def __init__(self):
348
+ self.rules: List[PermissionRule] = []
349
+
350
+ def add_workflow_permission(
351
+ self,
352
+ workflow_id: str,
353
+ permission: WorkflowPermission,
354
+ user_id: Optional[str] = None,
355
+ role: Optional[str] = None,
356
+ effect: PermissionEffect = PermissionEffect.ALLOW,
357
+ ):
358
+ """Add a workflow-level permission rule"""
359
+ rule = PermissionRule(
360
+ id=f"workflow_{workflow_id}_{permission.value}_{len(self.rules)}",
361
+ resource_type="workflow",
362
+ resource_id=workflow_id,
363
+ permission=permission,
364
+ effect=effect,
365
+ user_id=user_id,
366
+ role=role,
367
+ )
368
+ self.rules.append(rule)
369
+
370
+ def add_node_permission(
371
+ self,
372
+ workflow_id: str,
373
+ node_id: str,
374
+ permission: NodePermission,
375
+ user_id: Optional[str] = None,
376
+ role: Optional[str] = None,
377
+ effect: PermissionEffect = PermissionEffect.ALLOW,
378
+ masked_fields: Optional[List[str]] = None,
379
+ redirect_node: Optional[str] = None,
380
+ ):
381
+ """Add a node-level permission rule"""
382
+ rule = PermissionRule(
383
+ id=f"node_{workflow_id}_{node_id}_{permission.value}_{len(self.rules)}",
384
+ resource_type="node",
385
+ resource_id=node_id,
386
+ permission=permission,
387
+ effect=effect,
388
+ user_id=user_id,
389
+ role=role,
390
+ )
391
+
392
+ if masked_fields:
393
+ rule.conditions["masked_fields"] = masked_fields
394
+
395
+ if redirect_node:
396
+ rule.conditions["redirect_node"] = redirect_node
397
+
398
+ self.rules.append(rule)
399
+
400
+ def apply_to_manager(self, manager: AccessControlManager):
401
+ """Apply all rules to an access control manager"""
402
+ for rule in self.rules:
403
+ manager.add_rule(rule)
404
+
405
+
406
+ def execute_with_access_control(
407
+ workflow: Workflow,
408
+ user_context: UserContext,
409
+ parameters: Optional[Dict[str, Any]] = None,
410
+ access_config: Optional[AccessControlConfig] = None,
411
+ ) -> Tuple[Any, str]:
412
+ """
413
+ Convenience function to execute a workflow with access control.
414
+
415
+ Provides a simple way to execute workflows with access control without
416
+ manually creating runtime instances. Automatically applies access
417
+ configuration and manages the runtime lifecycle.
418
+
419
+ Args:
420
+ workflow: The workflow to execute
421
+ user_context: User context for access control decisions
422
+ parameters: Optional runtime parameters for workflow execution
423
+ access_config: Optional access control configuration to apply
424
+
425
+ Returns:
426
+ Tuple containing:
427
+ - result: The workflow execution result
428
+ - run_id: Unique identifier for this execution run
429
+
430
+ Raises:
431
+ PermissionError: If user lacks permission to execute workflow
432
+ ValueError: If workflow or user_context is invalid
433
+
434
+ Side Effects:
435
+ - Applies access control rules to global manager if config provided
436
+ - Logs audit events for access decisions
437
+ - Enables access control globally during execution
438
+
439
+ Example:
440
+ >>> from kailash.runtime.access_controlled import execute_with_access_control
441
+ >>> from kailash.access_control import UserContext
442
+ >>> from kailash.workflow import Workflow
443
+ >>>
444
+ >>> user = UserContext(user_id="123", tenant_id="abc", email="user@test.com", roles=["viewer"])
445
+ >>> workflow = Workflow(workflow_id="test", name="Test")
446
+ >>> # Function exists and can be called
447
+ >>> callable(execute_with_access_control)
448
+ True
449
+ """
450
+ # Set up access control if config provided
451
+ if access_config:
452
+ acm = get_access_control_manager()
453
+ access_config.apply_to_manager(acm)
454
+ acm.enabled = True # Enable access control
455
+
456
+ # Create runtime and execute
457
+ runtime = AccessControlledRuntime(user_context)
458
+ return runtime.execute(workflow, parameters)