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
51 lines
2.0 KiB
Python
51 lines
2.0 KiB
Python
"""The python packages node code may import.
|
|
|
|
A manifest in the flow store, a venv on the data volume, and one button that
|
|
brings the second in line with the first. Nothing here restarts the engine:
|
|
the worker pool retires its processes, and the next node call picks up the new
|
|
packages.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from fastapi.concurrency import run_in_threadpool
|
|
|
|
from app.api.deps import FlowControllerDep, WorkerPoolDep, get_current_user
|
|
from app.flow import modules
|
|
from app.flow.schemas import ApplyRequest, ApplyResult, ModulesInfo
|
|
|
|
router = APIRouter(
|
|
prefix="/modules", tags=["modules"], dependencies=[Depends(get_current_user)]
|
|
)
|
|
|
|
|
|
@router.get("/", response_model=ModulesInfo)
|
|
async def read_modules(controller: FlowControllerDep) -> Any:
|
|
"""What node code can import, and whether it matches the manifest."""
|
|
return await run_in_threadpool(modules.info, controller.store)
|
|
|
|
|
|
@router.post("/apply", response_model=ApplyResult)
|
|
async def apply_modules(
|
|
body: ApplyRequest, controller: FlowControllerDep, pool: WorkerPoolDep
|
|
) -> Any:
|
|
"""Install exactly these requirements, then hand them to the workers.
|
|
|
|
A manifest that does not resolve changes nothing: the venv is left as it
|
|
was and the stored manifest is only written once the install succeeded.
|
|
"""
|
|
ok, output = await run_in_threadpool(modules.sync, body.requirements)
|
|
if not ok:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=output or "These requirements could not be installed",
|
|
)
|
|
await run_in_threadpool(controller.store.write_requirements, body.requirements)
|
|
# Retire the workers first, so the rebuild compiles every node against the
|
|
# packages that were just installed — a node that could not import one is
|
|
# the reason this was called, and it stays red until it is built again.
|
|
pool.respawn_all()
|
|
await controller.reload()
|
|
return ApplyResult(ok=True, output=output)
|