abstractflow 0.1.0__py3-none-any.whl → 0.3.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.
- abstractflow/__init__.py +75 -95
- abstractflow/__main__.py +2 -0
- abstractflow/adapters/__init__.py +11 -0
- abstractflow/adapters/agent_adapter.py +124 -0
- abstractflow/adapters/control_adapter.py +615 -0
- abstractflow/adapters/effect_adapter.py +645 -0
- abstractflow/adapters/event_adapter.py +307 -0
- abstractflow/adapters/function_adapter.py +97 -0
- abstractflow/adapters/subflow_adapter.py +74 -0
- abstractflow/adapters/variable_adapter.py +317 -0
- abstractflow/cli.py +2 -0
- abstractflow/compiler.py +2027 -0
- abstractflow/core/__init__.py +5 -0
- abstractflow/core/flow.py +247 -0
- abstractflow/py.typed +2 -0
- abstractflow/runner.py +348 -0
- abstractflow/visual/__init__.py +43 -0
- abstractflow/visual/agent_ids.py +29 -0
- abstractflow/visual/builtins.py +789 -0
- abstractflow/visual/code_executor.py +214 -0
- abstractflow/visual/event_ids.py +33 -0
- abstractflow/visual/executor.py +2789 -0
- abstractflow/visual/interfaces.py +347 -0
- abstractflow/visual/models.py +252 -0
- abstractflow/visual/session_runner.py +168 -0
- abstractflow/visual/workspace_scoped_tools.py +261 -0
- abstractflow-0.3.0.dist-info/METADATA +413 -0
- abstractflow-0.3.0.dist-info/RECORD +32 -0
- {abstractflow-0.1.0.dist-info → abstractflow-0.3.0.dist-info}/licenses/LICENSE +2 -0
- abstractflow-0.1.0.dist-info/METADATA +0 -238
- abstractflow-0.1.0.dist-info/RECORD +0 -10
- {abstractflow-0.1.0.dist-info → abstractflow-0.3.0.dist-info}/WHEEL +0 -0
- {abstractflow-0.1.0.dist-info → abstractflow-0.3.0.dist-info}/entry_points.txt +0 -0
- {abstractflow-0.1.0.dist-info → abstractflow-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Sandboxed Python code execution for visual `Code` nodes.
|
|
2
|
+
|
|
3
|
+
This is a host-side utility used by the visual workflow compiler. It is kept in
|
|
4
|
+
the `abstractflow` package so visual workflows can be executed from other hosts
|
|
5
|
+
without importing the web backend implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
from typing import Any, Callable, Dict
|
|
12
|
+
|
|
13
|
+
# Try to import RestrictedPython, fall back to basic execution if not available
|
|
14
|
+
try:
|
|
15
|
+
from RestrictedPython import compile_restricted, safe_builtins
|
|
16
|
+
from RestrictedPython.Eval import default_guarded_getitem, default_guarded_getiter
|
|
17
|
+
from RestrictedPython.Guards import guarded_iter_unpack_sequence
|
|
18
|
+
|
|
19
|
+
RESTRICTED_PYTHON_AVAILABLE = True
|
|
20
|
+
except ImportError: # pragma: no cover
|
|
21
|
+
RESTRICTED_PYTHON_AVAILABLE = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CodeExecutionError(Exception):
|
|
25
|
+
"""Error during code execution."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_code(code: str) -> None:
|
|
29
|
+
"""Validate Python code for safety.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
CodeExecutionError: If code contains disallowed constructs.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
tree = ast.parse(code)
|
|
36
|
+
except SyntaxError as e:
|
|
37
|
+
raise CodeExecutionError(f"Syntax error: {e}") from e
|
|
38
|
+
|
|
39
|
+
for node in ast.walk(tree):
|
|
40
|
+
# Disallow imports
|
|
41
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
42
|
+
raise CodeExecutionError("Imports are not allowed")
|
|
43
|
+
|
|
44
|
+
# Disallow exec/eval
|
|
45
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
46
|
+
if node.func.id in ("exec", "eval", "compile", "__import__"):
|
|
47
|
+
raise CodeExecutionError(f"'{node.func.id}' is not allowed")
|
|
48
|
+
|
|
49
|
+
# Disallow dunder attributes
|
|
50
|
+
if isinstance(node, ast.Attribute) and node.attr.startswith("__") and node.attr.endswith("__"):
|
|
51
|
+
raise CodeExecutionError(f"Access to dunder attributes ('{node.attr}') is not allowed")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create_code_handler(code: str, function_name: str = "transform") -> Callable[[Any], Any]:
|
|
55
|
+
"""Create a handler function from user-provided Python code.
|
|
56
|
+
|
|
57
|
+
The code should define a function that takes input data and returns a result.
|
|
58
|
+
"""
|
|
59
|
+
validate_code(code)
|
|
60
|
+
|
|
61
|
+
if RESTRICTED_PYTHON_AVAILABLE:
|
|
62
|
+
return _create_restricted_handler(code, function_name)
|
|
63
|
+
return _create_basic_handler(code, function_name)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _create_restricted_handler(code: str, function_name: str) -> Callable[[Any], Any]:
|
|
67
|
+
"""Create handler using RestrictedPython for sandboxed execution."""
|
|
68
|
+
byte_code = compile_restricted(code, filename="<user_code>", mode="exec")
|
|
69
|
+
|
|
70
|
+
if getattr(byte_code, "errors", None):
|
|
71
|
+
errors = getattr(byte_code, "errors", None)
|
|
72
|
+
if isinstance(errors, list) and errors:
|
|
73
|
+
raise CodeExecutionError(f"Compilation errors: {'; '.join(errors)}")
|
|
74
|
+
|
|
75
|
+
def handler(input_data: Any) -> Any:
|
|
76
|
+
restricted_globals = {
|
|
77
|
+
"__builtins__": safe_builtins,
|
|
78
|
+
"_getiter_": default_guarded_getiter,
|
|
79
|
+
"_getitem_": default_guarded_getitem,
|
|
80
|
+
"_iter_unpack_sequence_": guarded_iter_unpack_sequence,
|
|
81
|
+
# Allow some safe built-ins
|
|
82
|
+
"len": len,
|
|
83
|
+
"str": str,
|
|
84
|
+
"int": int,
|
|
85
|
+
"float": float,
|
|
86
|
+
"bool": bool,
|
|
87
|
+
"list": list,
|
|
88
|
+
"dict": dict,
|
|
89
|
+
"tuple": tuple,
|
|
90
|
+
"set": set,
|
|
91
|
+
"range": range,
|
|
92
|
+
"enumerate": enumerate,
|
|
93
|
+
"zip": zip,
|
|
94
|
+
"map": map,
|
|
95
|
+
"filter": filter,
|
|
96
|
+
"sorted": sorted,
|
|
97
|
+
"reversed": reversed,
|
|
98
|
+
"min": min,
|
|
99
|
+
"max": max,
|
|
100
|
+
"sum": sum,
|
|
101
|
+
"abs": abs,
|
|
102
|
+
"round": round,
|
|
103
|
+
"isinstance": isinstance,
|
|
104
|
+
"type": type,
|
|
105
|
+
"print": lambda *args, **kwargs: None, # Silent print
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
local_vars: Dict[str, Any] = {}
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
exec(byte_code, restricted_globals, local_vars)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise CodeExecutionError(f"Execution error: {e}") from e
|
|
114
|
+
|
|
115
|
+
# `exec(..., globals, locals)` stores definitions in `locals`, but functions
|
|
116
|
+
# resolve globals against the `globals` dict. Make user-defined helpers
|
|
117
|
+
# (and other top-level values) available to `transform`.
|
|
118
|
+
reserved = {"__builtins__", "_getiter_", "_getitem_", "_iter_unpack_sequence_"}
|
|
119
|
+
for name, value in local_vars.items():
|
|
120
|
+
if name in reserved:
|
|
121
|
+
continue
|
|
122
|
+
if name.startswith("__") and name.endswith("__"):
|
|
123
|
+
continue
|
|
124
|
+
if name not in restricted_globals:
|
|
125
|
+
restricted_globals[name] = value
|
|
126
|
+
|
|
127
|
+
func = local_vars.get(function_name)
|
|
128
|
+
if func is None:
|
|
129
|
+
raise CodeExecutionError(f"Function '{function_name}' not defined in code")
|
|
130
|
+
if not callable(func):
|
|
131
|
+
raise CodeExecutionError(f"'{function_name}' is not a callable function")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
return func(input_data)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
raise CodeExecutionError(f"Runtime error: {e}") from e
|
|
137
|
+
|
|
138
|
+
return handler
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _create_basic_handler(code: str, function_name: str) -> Callable[[Any], Any]:
|
|
142
|
+
"""Create handler with basic (less secure) execution.
|
|
143
|
+
|
|
144
|
+
Used as fallback when RestrictedPython is not available.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
byte_code = compile(code, filename="<user_code>", mode="exec")
|
|
148
|
+
except SyntaxError as e:
|
|
149
|
+
raise CodeExecutionError(f"Syntax error: {e}") from e
|
|
150
|
+
|
|
151
|
+
def handler(input_data: Any) -> Any:
|
|
152
|
+
limited_globals = {
|
|
153
|
+
"__builtins__": {
|
|
154
|
+
"len": len,
|
|
155
|
+
"str": str,
|
|
156
|
+
"int": int,
|
|
157
|
+
"float": float,
|
|
158
|
+
"bool": bool,
|
|
159
|
+
"list": list,
|
|
160
|
+
"dict": dict,
|
|
161
|
+
"tuple": tuple,
|
|
162
|
+
"set": set,
|
|
163
|
+
"range": range,
|
|
164
|
+
"enumerate": enumerate,
|
|
165
|
+
"zip": zip,
|
|
166
|
+
"map": map,
|
|
167
|
+
"filter": filter,
|
|
168
|
+
"sorted": sorted,
|
|
169
|
+
"reversed": reversed,
|
|
170
|
+
"min": min,
|
|
171
|
+
"max": max,
|
|
172
|
+
"sum": sum,
|
|
173
|
+
"abs": abs,
|
|
174
|
+
"round": round,
|
|
175
|
+
"isinstance": isinstance,
|
|
176
|
+
"type": type,
|
|
177
|
+
"print": lambda *args, **kwargs: None,
|
|
178
|
+
"True": True,
|
|
179
|
+
"False": False,
|
|
180
|
+
"None": None,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
local_vars: Dict[str, Any] = {}
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
exec(byte_code, limited_globals, local_vars)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
raise CodeExecutionError(f"Execution error: {e}") from e
|
|
190
|
+
|
|
191
|
+
# Keep the same semantics as normal Python modules: helper functions and
|
|
192
|
+
# top-level constants defined alongside `transform()` should be visible
|
|
193
|
+
# at runtime. Avoid letting user code replace `__builtins__`.
|
|
194
|
+
reserved = {"__builtins__"}
|
|
195
|
+
for name, value in local_vars.items():
|
|
196
|
+
if name in reserved:
|
|
197
|
+
continue
|
|
198
|
+
if name.startswith("__") and name.endswith("__"):
|
|
199
|
+
continue
|
|
200
|
+
if name not in limited_globals:
|
|
201
|
+
limited_globals[name] = value
|
|
202
|
+
|
|
203
|
+
func = local_vars.get(function_name)
|
|
204
|
+
if func is None:
|
|
205
|
+
raise CodeExecutionError(f"Function '{function_name}' not defined in code")
|
|
206
|
+
if not callable(func):
|
|
207
|
+
raise CodeExecutionError(f"'{function_name}' is not a callable function")
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
return func(input_data)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
raise CodeExecutionError(f"Runtime error: {e}") from e
|
|
213
|
+
|
|
214
|
+
return handler
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Deterministic workflow IDs for VisualFlow custom event listeners.
|
|
2
|
+
|
|
3
|
+
Visual "On Event" nodes are compiled into dedicated listener workflows and started
|
|
4
|
+
alongside the main workflow run.
|
|
5
|
+
|
|
6
|
+
IDs must be stable across hosts so a VisualFlow JSON document can be executed
|
|
7
|
+
outside the web editor (CLI, AbstractCode, third-party apps).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_SAFE_ID_RE = re.compile(r"[^a-zA-Z0-9_-]+")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _sanitize(value: str) -> str:
|
|
19
|
+
value = str(value or "").strip()
|
|
20
|
+
if not value:
|
|
21
|
+
return "unknown"
|
|
22
|
+
value = _SAFE_ID_RE.sub("_", value)
|
|
23
|
+
return value or "unknown"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def visual_event_listener_workflow_id(*, flow_id: str, node_id: str) -> str:
|
|
27
|
+
"""Return the workflow_id used for a VisualFlow `on_event` listener workflow."""
|
|
28
|
+
return f"visual_event_listener_{_sanitize(flow_id)}_{_sanitize(node_id)}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|