Run python nodes out of process, with modules of their own

User code no longer execs in the engine. A pool of persistent worker
subprocesses speaks one JSON object per line; the controller installs a
proxy as the node's function, so every execution path funnels through it
and the pipeline is untouched. A crash costs one subprocess, a per-node
timeout is a kill, and cancelling from the canvas is that same kill.

The workers run a venv of the user's own on the data volume, filled from
a pip manifest versioned beside the flows. Applying it retires the
workers and rebuilds, so a package lands without restarting the engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
This commit is contained in:
2026-08-16 21:43:36 +02:00
co-authored by Claude Fable 5
parent 979c9d3c1f
commit f300c43f3a
27 changed files with 1536 additions and 46 deletions
+27
View File
@@ -238,3 +238,30 @@ async def pause_flow(name: str) -> Any:
async def resume_flow(name: str) -> Any:
"""Let a paused flow carry on, running whatever was held back."""
return await _call("POST", f"/flows/{name}/resume")
@mcp.tool()
async def cancel_node(name: str, node_id: str) -> Any:
"""Stop a node's code while it is running. Nothing to stop is not an error."""
return await _call("POST", f"/flows/{name}/nodes/{node_id}/cancel")
# -----------------------------------------------------------------------------
# Modules
# -----------------------------------------------------------------------------
@mcp.tool()
async def get_modules() -> Any:
"""The python packages node code can import, and the manifest asking for them."""
return await _call("GET", "/modules/")
@mcp.tool()
async def apply_modules(requirements: str) -> Any:
"""Install exactly these requirements, one pip line each.
This replaces the whole manifest: a package left out is uninstalled. A
manifest that does not resolve changes nothing.
"""
return await _call("POST", "/modules/apply", json={"requirements": requirements})