From 73edff0d72cfcd02516ed34becd40ecfb5915827 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 05:03:20 +0000 Subject: [PATCH] docs: report on integrating smolvm as a Langflow runtime sandbox Maps current Langflow code-execution paths (custom components, Python REPL tools, code structured tools) and proposes a pluggable CodeExecutionService backed by smolvm microVMs to replace unrestricted exec(). --- docs/reports/smolvm-langflow-runtime.md | 175 ++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 docs/reports/smolvm-langflow-runtime.md diff --git a/docs/reports/smolvm-langflow-runtime.md b/docs/reports/smolvm-langflow-runtime.md new file mode 100644 index 0000000000..87ac3944a1 --- /dev/null +++ b/docs/reports/smolvm-langflow-runtime.md @@ -0,0 +1,175 @@ +# Using smolvm in the Langflow Runtime + +**Status:** Exploratory report +**Date:** 2026-04-19 +**Subject:** Evaluating [smolvm](https://github.com/smol-machines/smolvm) as a sandboxed execution backend for Langflow flows. + +--- + +## 1. TL;DR + +Langflow's runtime executes user-supplied Python (Custom Components, Python REPL tools, Python Code Structured Tools) with **unrestricted `exec()` against host globals** — full filesystem, network, and `importlib` access. smolvm offers sub-200 ms libkrun/Hypervisor.framework microVMs with network-off-by-default isolation and an OCI image model. Wiring smolvm in as a **pluggable code-execution service** (Langflow already supports this pattern) would convert the most dangerous code paths from "trust the author" to "hardware-isolated, network-deny, allowlisted egress" without changing the component-author UX. + +The integration is feasible and the surface area is small (~3 call sites). The main gap is that smolvm currently exposes a CLI and an "Embeddable SDK" advertised in the README but not yet documented; a CLI-driven adapter is the pragmatic v1. + +--- + +## 2. What smolvm provides + +Source: (v0.5.19, ~1.8k stars, active). + +| Property | Value | +|---|---| +| Isolation | Real hardware virtualization — Hypervisor.framework (macOS), KVM (Linux) | +| VMM | libkrun + libkrunfw kernel | +| Image format | OCI (Docker Hub compatible, no Docker daemon) | +| Cold start | < 200 ms | +| Memory | Elastic via virtio-balloon; default 4 vCPU / 8 GiB | +| Network | **Off by default**; opt-in `--net`; egress allowlist via `--allow-host` / `[network].allow_hosts`; TCP/UDP only | +| Filesystem | Directory-only volume mounts (`./src:/app`) | +| Auth | SSH agent forwarding (`SSH_AUTH_SOCK`) — keys stay on host side of the hypervisor | +| Portability | Pack stateful VMs into a single `.smolmachine` binary | +| Configuration | `Smolfile` (TOML): `image`, `[network]`, `[dev]`, `[auth]` | +| Surface | CLI (`smolvm machine run | create | start | stop | exec`, `smolvm pack create`); README claims "Embeddable SDK — Yes" but bindings are not yet documented | + +Key CLI shapes relevant to integration: + +```bash +smolvm machine run --image python:3.12-alpine -it -- python -c "..." +smolvm machine create --name lf-comp -s Smolfile +smolvm machine exec --name lf-comp -- python /app/run.py +``` + +--- + +## 3. Where Langflow runs user code today + +All paths below are in this repo. They were mapped end-to-end so the integration points are concrete. + +### 3.1 Graph engine (the orchestrator) + +- `src/lfx/src/lfx/graph/graph/base.py:356` — `Graph.async_start()` async generator driving the run. +- `src/lfx/src/lfx/graph/graph/base.py:1438` — `Graph.astep()` dequeues the next vertex. +- `src/lfx/src/lfx/graph/graph/base.py:1534` — `Graph.build_vertex()` cache + build orchestration. +- `src/lfx/src/lfx/graph/vertex/base.py:782` — `Vertex.build()` per-vertex driver. +- `src/lfx/src/lfx/graph/vertex/base.py:701` — `Vertex._build_results()` calls + `initialize.loading.get_instance_results()`. + +### 3.2 Custom Component execution (the unsandboxed hot path) + +- `src/lfx/src/lfx/interface/initialize/loading.py:28` — `instantiate_class()` parses vertex code via `eval_custom_component_code()`. +- `src/lfx/src/lfx/interface/initialize/loading.py:306` — `build_custom_component()` awaits the component's `build()`. +- `src/lfx/src/lfx/custom/eval.py:9` — `eval_custom_component_code()` entry. +- `src/lfx/src/lfx/custom/validate.py:248` — `create_class()` parses to AST, prepares globals, and **`exec()`s the class definition into a copy of host globals** (lines 271–278). No RestrictedPython, no seccomp, no subprocess. + +### 3.3 User-facing "code tool" components + +- `src/lfx/src/lfx/components/utilities/python_repl_core.py:72` — wraps `langchain_experimental.utilities.PythonREPL`; only protection is an import allowlist string. +- `src/lfx/src/lfx/components/tools/python_repl.py:73` — same pattern, wrapped as a `StructuredTool`. +- `src/lfx/src/lfx/components/tools/python_code_structured_tool.py:140` — `exec()`s arbitrary user-supplied tool code into globals after AST-loading imports. No isolation. + +### 3.4 Plug-in surface that lets us swap backends + +- `src/lfx/src/lfx/services/manager.py:43` — `ServiceManager`. Discovery precedence: **config file > `@register_service` decorator > entry points**. One service per `ServiceType` (singleton), plus multi-adapter registries keyed by string. +- `src/lfx/PLUGGABLE_SERVICES.md` — the documented contract. + +This is the natural seam: a `CodeExecutionService` registered here can be selected by config without touching graph code. + +--- + +## 4. Threat model gap that smolvm closes + +| Capability today | Risk | smolvm mitigation | +|---|---|---| +| `exec(user_code, globals_copy)` in Custom Components | Read/write host FS, exfiltrate env vars / `.aws`, mount points | Guest FS only; explicit volume mounts | +| Unrestricted `importlib.import_module` | Pull arbitrary deps, exec on import | OCI image is the closed world | +| Outbound HTTP from any component | Data exfil, SSRF against internal services | Network off by default; per-flow allowlist | +| Long-running / runaway CPU in user code | Backend process pinned, no quota | vCPU/memory caps per VM | +| State leaks across runs (module cache, `os.environ` mutation) | Cross-tenant pollution | Per-run ephemeral VM, or per-tenant persistent VM | +| SSH/cloud creds reachable by user code | Credential theft | `--ssh-agent` forwards the socket; keys never enter guest | + +--- + +## 5. Proposed integration + +### 5.1 Architecture + +Introduce `ServiceType.CODE_EXECUTION` with two implementations: + +1. `InProcessCodeExecutionService` — current behavior, default, preserves dev ergonomics. +2. `SmolvmCodeExecutionService` — opt-in via `lfx.toml`: + +```toml +[services] +code_execution = "lfx_smolvm.service:SmolvmCodeExecutionService" + +[services.code_execution.config] +image = "ghcr.io/langflow/runtime-py312:latest" +default_net = false +allow_hosts = ["api.openai.com", "api.anthropic.com"] +mode = "ephemeral" # or "persistent-per-flow" +cpus = 2 +mem_mib = 1024 +``` + +### 5.2 Where to intercept + +The smallest viable change patches one function: + +- **`src/lfx/src/lfx/interface/initialize/loading.py:306` (`build_custom_component`)** — instead of calling the component's `build()` in-process, marshal `(component_code, params, inputs)` over the boundary, run inside the VM, and return `(component, build_result, artifact)` reconstructed from the VM's serialized output. + +For the explicit code-tool components in §3.3, replace the `PythonREPL` / `exec` calls with a thin `code_exec_service.run(code, allowlist)` call. These are the highest-value, lowest-risk wins because they already represent untrusted-by-design code. + +### 5.3 v1 transport (CLI-driven) + +Until smolvm publishes Python bindings, drive it via subprocess: + +```python +# Ephemeral run, stdin = code, stdout = pickled/JSON result +proc = await asyncio.create_subprocess_exec( + "smolvm", "machine", "run", + "--image", cfg.image, + "--cpus", str(cfg.cpus), "--mem", f"{cfg.mem_mib}M", + *(["--net"] if needs_net else []), + *[f"--allow-host={h}" for h in cfg.allow_hosts], + "--", "python", "/app/runner.py", + stdin=PIPE, stdout=PIPE, stderr=PIPE, +) +out, err = await proc.communicate(payload) +``` + +A small `runner.py` baked into the image deserializes the request, executes the user code, and serializes the result + artifacts. Volume-mount a tmpdir for large artifacts to avoid stdio pressure. + +### 5.4 Persistent vs ephemeral + +- **Ephemeral (`smolvm machine run`)**: best isolation, ~200 ms cold start per vertex. Acceptable for human-in-the-loop builds; painful for graphs with many small vertices. +- **Persistent-per-flow (`smolvm machine create` + `exec`)**: one VM per flow run, vertices `exec` into it. Amortizes start cost, keeps tenant isolation. Recommended default. +- **Persistent-per-tenant**: warmest, but reintroduces cross-vertex state leakage inside the tenant boundary. + +### 5.5 Async fit + +Langflow is asyncio-native (`Graph.astep`, `Vertex.build`). `asyncio.create_subprocess_exec` integrates cleanly; no thread offload needed beyond the existing `Graph.start()` sync wrapper at `base.py:429`. + +--- + +## 6. Open questions / risks + +1. **SDK availability.** The README advertises an embeddable SDK but doesn't document bindings. Worth opening an issue upstream before committing to non-CLI transport. +2. **macOS dev parity.** Hypervisor.framework requires macOS; KVM requires Linux + `/dev/kvm`. Windows dev (WSL2) and CI containers (nested virt) need validation. Fall back to `InProcessCodeExecutionService` when virtualization is unavailable. +3. **Component <-> host data marshaling.** Langflow components return rich Python objects (LangChain `Runnable`, custom classes). The boundary forces serialization — likely cloudpickle with an allowlist, or a typed `Message`/`Data` envelope already used at component boundaries. This is the biggest design question. +4. **Image distribution.** A signed `langflow/runtime-py312` image needs to ship with the langchain/langflow deps the average component imports, or component install latency dominates the 200 ms boot. +5. **Streaming.** Many components stream tokens. Stdout-line streaming through the VM is workable but needs a framing protocol (length-prefixed frames or NDJSON). +6. **Filesystem-touching components.** File loaders, vector stores with on-disk persistence — these need explicit mount policy per component category, not blanket deny. +7. **Observability.** Tracing/logging spans currently propagate via in-process context vars; need to forward `trace_id` across the boundary. + +--- + +## 7. Recommendation + +Land in three steps: + +1. **Define `ServiceType.CODE_EXECUTION`** and refactor the three call sites in §3 to go through it, with the in-process implementation as default. **No behavior change**, but the seam exists. +2. **Ship `lfx-smolvm` as an optional package** with the CLI-driven adapter, persistent-per-flow mode, and a baseline runtime image. Gate behind config; document the Smolfile/allowlist UX. +3. **Default to smolvm for the "code tool" components** (`python_repl_core`, `python_repl`, `python_code_structured_tool`) once the SDK or stable CLI contract lands — these are the components where users most expect untrusted input and where the isolation upside is unambiguous. + +Step 1 is worth doing regardless of smolvm — it cleans up the runtime and unlocks other backends (Firecracker, gVisor, WASM).