fw-nodes-code 0.0.1a1__tar.gz

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.
@@ -0,0 +1,20 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: Public-Mirrors/actions_checkout@v6
13
+
14
+ - name: Install uv
15
+ run: curl -LsSf https://astral.sh/uv/install.sh | sh
16
+
17
+ - name: Build and publish
18
+ run: |
19
+ uv build
20
+ uv publish --token ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,41 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+
8
+ # Virtual environments
9
+ .venv/
10
+ venv/
11
+ env/
12
+ ENV/
13
+
14
+ # Environment files
15
+ .env
16
+ .env.local
17
+
18
+ # Testing
19
+ .pytest_cache/
20
+ .coverage
21
+ htmlcov/
22
+ coverage.xml
23
+
24
+ # Build artifacts
25
+ dist/
26
+ build/
27
+ *.egg-info/
28
+ .eggs/
29
+
30
+ # IDE
31
+ .idea/
32
+ .vscode/
33
+ *.swp
34
+ *.swo
35
+
36
+ # OS
37
+ .DS_Store
38
+ Thumbs.db
39
+
40
+ # uv
41
+ uv.lock
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Eddy Hintze
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: fw-nodes-code
3
+ Version: 0.0.1a1
4
+ Summary: Code execution nodes for Flowire workflow automation
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: flowire-sdk>=0.0.1a2
9
+ Requires-Dist: httpx>=0.26.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
12
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
13
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Flowire Code Nodes
17
+
18
+ Code execution node package for Flowire workflow automation.
19
+
20
+ This package is intentionally separate from `fw-nodes-core` so servers can opt into code execution support by installing this package.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ cd flowire-app/backend
26
+ uv pip install fw-nodes-code
27
+ ```
28
+
29
+ Enable it in your `.env` file:
30
+
31
+ ```bash
32
+ INSTALLED_NODE_PACKAGES=fw-nodes-core,fw-nodes-code
33
+ ```
34
+
35
+ ## Included Node
36
+
37
+ | Node | Description |
38
+ |------|-------------|
39
+ | `Code` | Execute JavaScript or Python code in an isolated sandbox |
40
+
41
+ The node supports a `runtime` input (default `latest`) so you can route
42
+ different versions to different sandbox services (for example `3.13`, `3.9`, `20`).
43
+
44
+ ## Sandbox Services
45
+
46
+ This package includes the sandbox runtimes used by the Code node:
47
+
48
+ - `js-sandbox/` - JavaScript sandbox (isolated-vm + Fastify)
49
+ - `py-sandbox/` - Python sandbox (FastAPI + subprocess execution)
50
+
51
+ ### Local sandbox commands
52
+
53
+ ```bash
54
+ cd fw-nodes-code
55
+ just js-sandbox-install
56
+ just py-sandbox-install
57
+ just js-sandbox
58
+ just py-sandbox
59
+ ```
60
+
61
+ ## Environment Variables
62
+
63
+ - `CODE_NODE_SANDBOX_URL_MAP` - JSON object mapping runtime keys to sandbox URLs.
64
+
65
+ Example:
66
+
67
+ ```bash
68
+ CODE_NODE_SANDBOX_URL_MAP='{
69
+ "javascript@latest":"http://localhost:3100",
70
+ "javascript@20":"http://localhost:3110",
71
+ "python@latest":"http://localhost:3200",
72
+ "python@3.13":"http://localhost:3213",
73
+ "python@3.9":"http://localhost:3209"
74
+ }'
75
+ ```
76
+
77
+ Runtime keys are resolved as:
78
+
79
+ 1. `{language}@{runtime}` (for example `python@3.13`)
80
+ 2. `{language}@latest`
81
+ 3. (no fallback route; configuration must exist in the map)
82
+
83
+ If `CODE_NODE_SANDBOX_URL_MAP` is not set, defaults are used:
84
+
85
+ - `javascript@latest -> http://localhost:3100`
86
+ - `python@latest -> http://localhost:3200`
87
+
88
+ ## Development
89
+
90
+ ```bash
91
+ just install
92
+ just js-sandbox-install
93
+ just py-sandbox-install
94
+ just js-sandbox
95
+ just py-sandbox
96
+ just test
97
+ just lint
98
+ ```
@@ -0,0 +1,83 @@
1
+ # Flowire Code Nodes
2
+
3
+ Code execution node package for Flowire workflow automation.
4
+
5
+ This package is intentionally separate from `fw-nodes-core` so servers can opt into code execution support by installing this package.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ cd flowire-app/backend
11
+ uv pip install fw-nodes-code
12
+ ```
13
+
14
+ Enable it in your `.env` file:
15
+
16
+ ```bash
17
+ INSTALLED_NODE_PACKAGES=fw-nodes-core,fw-nodes-code
18
+ ```
19
+
20
+ ## Included Node
21
+
22
+ | Node | Description |
23
+ |------|-------------|
24
+ | `Code` | Execute JavaScript or Python code in an isolated sandbox |
25
+
26
+ The node supports a `runtime` input (default `latest`) so you can route
27
+ different versions to different sandbox services (for example `3.13`, `3.9`, `20`).
28
+
29
+ ## Sandbox Services
30
+
31
+ This package includes the sandbox runtimes used by the Code node:
32
+
33
+ - `js-sandbox/` - JavaScript sandbox (isolated-vm + Fastify)
34
+ - `py-sandbox/` - Python sandbox (FastAPI + subprocess execution)
35
+
36
+ ### Local sandbox commands
37
+
38
+ ```bash
39
+ cd fw-nodes-code
40
+ just js-sandbox-install
41
+ just py-sandbox-install
42
+ just js-sandbox
43
+ just py-sandbox
44
+ ```
45
+
46
+ ## Environment Variables
47
+
48
+ - `CODE_NODE_SANDBOX_URL_MAP` - JSON object mapping runtime keys to sandbox URLs.
49
+
50
+ Example:
51
+
52
+ ```bash
53
+ CODE_NODE_SANDBOX_URL_MAP='{
54
+ "javascript@latest":"http://localhost:3100",
55
+ "javascript@20":"http://localhost:3110",
56
+ "python@latest":"http://localhost:3200",
57
+ "python@3.13":"http://localhost:3213",
58
+ "python@3.9":"http://localhost:3209"
59
+ }'
60
+ ```
61
+
62
+ Runtime keys are resolved as:
63
+
64
+ 1. `{language}@{runtime}` (for example `python@3.13`)
65
+ 2. `{language}@latest`
66
+ 3. (no fallback route; configuration must exist in the map)
67
+
68
+ If `CODE_NODE_SANDBOX_URL_MAP` is not set, defaults are used:
69
+
70
+ - `javascript@latest -> http://localhost:3100`
71
+ - `python@latest -> http://localhost:3200`
72
+
73
+ ## Development
74
+
75
+ ```bash
76
+ just install
77
+ just js-sandbox-install
78
+ just py-sandbox-install
79
+ just js-sandbox
80
+ just py-sandbox
81
+ just test
82
+ just lint
83
+ ```
@@ -0,0 +1,3 @@
1
+ """Code nodes for Flowire workflow automation."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ """Code node implementations for Flowire.
2
+
3
+ Nodes in this package are auto-discovered via entry points.
4
+ """
@@ -0,0 +1,296 @@
1
+ """Code node for executing custom code in sandboxed environments."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+ import httpx
10
+ from flowire_sdk import BaseNode, BaseNodeOutput, CodeEditorContract, NodeExecutionContext, NodeMetadata
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ JS_DEFAULT_CODE = (
16
+ '// Available: $node["node-id"], $flow\n// Return your output as an object\n\nreturn {\n result: "hello"\n}'
17
+ )
18
+
19
+ PYTHON_DEFAULT_CODE = (
20
+ '# Available: nodes["node-id"], flow\n'
21
+ "# Assign your output to 'result'\n"
22
+ "\n"
23
+ "result = {\n"
24
+ ' "message": "Hello from Python",\n'
25
+ "}"
26
+ )
27
+
28
+
29
+ class Language(str, Enum):
30
+ """Supported code execution languages."""
31
+
32
+ JAVASCRIPT = "javascript"
33
+ PYTHON = "python"
34
+
35
+
36
+ SUPPORTED_LANGUAGE_VALUES = {language.value for language in Language}
37
+
38
+
39
+ DEFAULT_SANDBOX_URL_MAP: dict[str, str] = {
40
+ "javascript@latest": "http://localhost:3100",
41
+ "python@latest": "http://localhost:3200",
42
+ }
43
+
44
+
45
+ def _normalize_runtime(runtime: str | None) -> str:
46
+ """Normalize runtime hint to a stable lower-case token."""
47
+ normalized = (runtime or "latest").strip().lower()
48
+ return normalized or "latest"
49
+
50
+
51
+ def _parse_map_key(key: str) -> tuple[str, str] | None:
52
+ """Parse map key into language/runtime tokens."""
53
+ token = key.strip().lower()
54
+ if not token:
55
+ return None
56
+ if "@" in token:
57
+ language_token, runtime_token = token.split("@", 1)
58
+ else:
59
+ language_token, runtime_token = token, "latest"
60
+
61
+ language_token = language_token.strip()
62
+ runtime_token = _normalize_runtime(runtime_token)
63
+
64
+ if language_token not in SUPPORTED_LANGUAGE_VALUES:
65
+ return None
66
+ return language_token, runtime_token
67
+
68
+
69
+ def _load_sandbox_url_map() -> dict[str, str]:
70
+ """Load language/runtime -> sandbox URL mapping from environment."""
71
+ raw = os.environ.get("CODE_NODE_SANDBOX_URL_MAP", "").strip()
72
+ if not raw:
73
+ return DEFAULT_SANDBOX_URL_MAP.copy()
74
+
75
+ try:
76
+ parsed = json.loads(raw)
77
+ except json.JSONDecodeError:
78
+ logger.warning("Invalid CODE_NODE_SANDBOX_URL_MAP JSON; using default sandbox routing")
79
+ return DEFAULT_SANDBOX_URL_MAP.copy()
80
+
81
+ if not isinstance(parsed, dict):
82
+ logger.warning("CODE_NODE_SANDBOX_URL_MAP must be a JSON object; using default sandbox routing")
83
+ return DEFAULT_SANDBOX_URL_MAP.copy()
84
+
85
+ result: dict[str, str] = {}
86
+ for key, value in parsed.items():
87
+ if not isinstance(key, str) or not isinstance(value, str):
88
+ continue
89
+ parsed_key = _parse_map_key(key)
90
+ if not parsed_key:
91
+ continue
92
+ language_token, runtime_token = parsed_key
93
+ normalized_key = f"{language_token}@{runtime_token}"
94
+ normalized_url = value.strip()
95
+ if normalized_key and normalized_url:
96
+ result[normalized_key] = normalized_url
97
+
98
+ if not result:
99
+ logger.warning("CODE_NODE_SANDBOX_URL_MAP had no valid entries; using default sandbox routing")
100
+ return DEFAULT_SANDBOX_URL_MAP.copy()
101
+
102
+ return result
103
+
104
+
105
+ def _build_editor_options(url_map: dict[str, str]) -> tuple[list[str], dict[str, list[str]]]:
106
+ """Derive language/runtime dropdown options from sandbox map keys."""
107
+ language_order: list[str] = []
108
+ runtime_options: dict[str, list[str]] = {}
109
+
110
+ for key in url_map:
111
+ parsed_key = _parse_map_key(key)
112
+ if not parsed_key:
113
+ continue
114
+ language_token, runtime_token = parsed_key
115
+
116
+ if language_token not in language_order:
117
+ language_order.append(language_token)
118
+
119
+ runtimes = runtime_options.setdefault(language_token, [])
120
+ if runtime_token not in runtimes:
121
+ if runtime_token == "latest":
122
+ runtimes.insert(0, runtime_token)
123
+ else:
124
+ runtimes.append(runtime_token)
125
+
126
+ if not language_order:
127
+ language_order = [Language.JAVASCRIPT.value, Language.PYTHON.value]
128
+ runtime_options = {
129
+ Language.JAVASCRIPT.value: ["latest"],
130
+ Language.PYTHON.value: ["latest"],
131
+ }
132
+
133
+ return language_order, runtime_options
134
+
135
+
136
+ def _build_code_editor_contract() -> CodeEditorContract:
137
+ """Build code editor metadata contract including language/runtime dropdown options."""
138
+ url_map = _load_sandbox_url_map()
139
+ language_options, runtime_options = _build_editor_options(url_map)
140
+
141
+ return CodeEditorContract(
142
+ code_field="code",
143
+ language_field="language",
144
+ runtime_field="runtime",
145
+ code_format="code",
146
+ language_defaults={
147
+ "javascript": JS_DEFAULT_CODE,
148
+ "python": PYTHON_DEFAULT_CODE,
149
+ },
150
+ language_options=language_options,
151
+ runtime_options=runtime_options,
152
+ )
153
+
154
+
155
+ def _resolve_sandbox_url(language: Language, runtime: str | None) -> tuple[str, str]:
156
+ """Resolve target sandbox URL from runtime mapping."""
157
+ runtime_token = _normalize_runtime(runtime)
158
+ candidates = [f"{language.value}@{runtime_token}"]
159
+ if runtime_token != "latest":
160
+ candidates.append(f"{language.value}@latest")
161
+
162
+ url_map = _load_sandbox_url_map()
163
+ for key in candidates:
164
+ url = url_map.get(key)
165
+ if url:
166
+ return url, key
167
+
168
+ available_routes = sorted(url_map.keys())
169
+ raise RuntimeError(
170
+ "No sandbox URL configured for "
171
+ f"{language.value}@{runtime_token}. "
172
+ "Set CODE_NODE_SANDBOX_URL_MAP with a matching route. "
173
+ f"Available routes: {available_routes}"
174
+ )
175
+
176
+
177
+ class CodeNodeInput(BaseModel):
178
+ """Input schema for Code node."""
179
+
180
+ language: Language = Field(
181
+ default=Language.JAVASCRIPT,
182
+ description="Programming language for the code",
183
+ json_schema_extra={
184
+ "x-code-defaults": {
185
+ "javascript": JS_DEFAULT_CODE,
186
+ "python": PYTHON_DEFAULT_CODE,
187
+ }
188
+ },
189
+ )
190
+ code: str = Field(
191
+ default=JS_DEFAULT_CODE,
192
+ description='Code to execute. Use $node["id"]/$flow (JS) or nodes["id"]/flow (Python).',
193
+ json_schema_extra={"format": "code"},
194
+ )
195
+ runtime: str = Field(
196
+ default="latest",
197
+ description=("Runtime version hint for selecting the sandbox backend, e.g. 'latest', '3.13', '3.9', or '20'."),
198
+ )
199
+ timeout: int = Field(
200
+ default=10,
201
+ ge=1,
202
+ le=60,
203
+ description="Maximum execution time in seconds",
204
+ )
205
+
206
+
207
+ class CodeNodeOutput(BaseNodeOutput):
208
+ """Output schema for Code node - allows arbitrary fields from user code."""
209
+
210
+ model_config = ConfigDict(extra="allow")
211
+
212
+
213
+ class CodeNode(BaseNode):
214
+ """Execute custom code in a sandboxed environment."""
215
+
216
+ input_schema = CodeNodeInput
217
+ output_schema = CodeNodeOutput
218
+
219
+ metadata = NodeMetadata(
220
+ name="Code",
221
+ description="Execute custom code with access to workflow data",
222
+ category="developer",
223
+ icon="<>",
224
+ color="#1e1e1e",
225
+ display_component="code",
226
+ code_editor=_build_code_editor_contract(),
227
+ )
228
+
229
+ async def execute_logic(
230
+ self,
231
+ validated_inputs: dict[str, Any],
232
+ context: NodeExecutionContext,
233
+ ) -> CodeNodeOutput:
234
+ """Execute code in the appropriate sandbox."""
235
+ language = Language(validated_inputs.get("language", Language.JAVASCRIPT))
236
+ code = validated_inputs["code"]
237
+ runtime = validated_inputs.get("runtime", "latest")
238
+ timeout = validated_inputs.get("timeout", 10)
239
+
240
+ sandbox_url, sandbox_route = _resolve_sandbox_url(language, runtime)
241
+
242
+ # Build the request payload
243
+ # js-sandbox expects timeout in milliseconds, py-sandbox in seconds
244
+ sandbox_timeout = timeout * 1000 if language == Language.JAVASCRIPT else timeout
245
+ payload = {
246
+ "language": language.value,
247
+ "runtime": runtime,
248
+ "code": code,
249
+ "input": context.node_results,
250
+ "nodeResults": context.node_results,
251
+ "flow": {
252
+ "execution_id": context.execution_id,
253
+ "flow_id": context.workflow_id,
254
+ "project_id": context.project_id,
255
+ "node_id": context.node_id,
256
+ },
257
+ "timeout": sandbox_timeout,
258
+ }
259
+
260
+ # Call the sandbox
261
+ async with httpx.AsyncClient() as client:
262
+ try:
263
+ response = await client.post(
264
+ f"{sandbox_url}/execute",
265
+ json=payload,
266
+ timeout=timeout + 5, # Add buffer for network
267
+ )
268
+ response.raise_for_status()
269
+ result = response.json()
270
+ except httpx.ConnectError as e:
271
+ raise RuntimeError(
272
+ "Could not connect to code sandbox service for "
273
+ f"{language.value}@{_normalize_runtime(runtime)} "
274
+ f"(route '{sandbox_route}' -> {sandbox_url}). "
275
+ "Configure CODE_NODE_SANDBOX_URL_MAP."
276
+ ) from e
277
+ except httpx.TimeoutException as e:
278
+ raise RuntimeError(f"Code execution timed out after {timeout} seconds") from e
279
+
280
+ # Check for execution errors
281
+ if not result.get("success"):
282
+ error = result.get("error", {})
283
+ error_msg = error.get("message", "Unknown error")
284
+ error_stack = error.get("stack", "")
285
+ raise RuntimeError(f"Code execution failed: {error_msg}\n{error_stack}")
286
+
287
+ # Build output from result
288
+ output_data = result.get("result", {})
289
+ if not isinstance(output_data, dict):
290
+ output_data = {"result": output_data}
291
+
292
+ # Include execution metadata
293
+ output_data["_logs"] = result.get("logs", [])
294
+ output_data["_duration"] = result.get("duration", 0)
295
+
296
+ return CodeNodeOutput(**output_data)
@@ -0,0 +1,2 @@
1
+ node_modules/
2
+ dist/
@@ -0,0 +1 @@
1
+ 22
@@ -0,0 +1,16 @@
1
+ FROM node:22-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Install build dependencies for isolated-vm (needs python, make, g++)
6
+ RUN apk add --no-cache python3 make g++ curl
7
+
8
+ COPY package*.json ./
9
+ RUN npm ci
10
+
11
+ COPY . .
12
+ RUN npm run build
13
+
14
+ EXPOSE 3100
15
+
16
+ CMD ["npm", "start"]