This page describes how a pipeline run is bounded, cancelled, and protected in the current implementation. It is deliberately explicit about what the engine does not do, because the boundaries matter when you reason about failure.
The engine runs entirely in one process. run_with_graph() dispatches the
graph onto a concurrent.futures.ThreadPoolExecutor
(pipeline/runtime.py),
sized by settings.sandbox_exec_workers (SANDBOX_EXEC_WORKERS, default 4).
Threads share one interpreter and one address space.
Two consequences follow, and neither is a sandbox property:
abort()) terminates the whole
process, including the orchestrator. There is no disposable worker to absorb
it.The setting is named sandbox_exec_workers for historical reasons. It controls
thread-pool width, not isolation.
This is the engine’s real safety property, and it holds. The LLM emits an
Abstract Syntax Tree, never SQL text. The AST passes through
LogicalValidatorNode
(pipeline/nodes/validator/node.py)
before the generator is reached. That node:
sqlglot.optimizer.qualify;RBAC.get_allowed_tables()
(auth/rbac.py),
failing closed when no datasource ID is present.An unvalidated plan never becomes SQL, so it never reaches an adapter. This is a correctness and authorization gate, not a containment boundary: it prevents bad SQL from being generated, it does not contain a driver that misbehaves on SQL that passed.
Each invocation constructs its own CancellationToken
(common/cancellation.py)
and threads it to nodes through
config={"configurable": {"cancellation_token": token}}. Because the token is
per-run, cancelling one run never affects another.
Nodes observe the token and unwind cooperatively, so a cancelled run returns a
normal result carrying a CANCELLED PipelineError rather than raising.
run_with_graph() cancels the token on SIGINT/SIGTERM, and on Windows
interactive terminals also on Ctrl+X.
Cancellation is cooperative: a node blocked inside a driver call does not abandon that call. The token is checked between and within nodes, not inside third-party I/O.
settings.global_timeout_sec (GLOBAL_TIMEOUT_SEC, default 60) is enforced by
future.result(timeout=...) in run_with_graph(). On expiry the caller gets a
PIPELINE_TIMEOUT error and a user-facing message.
The timeout bounds how long the caller waits, not how long the work runs. Since the executor is a thread pool, the underlying thread is not killed and may continue until its own I/O completes.
VECTOR_BREAKER
(common/resilience.py)
is the only breaker the system defines (fail_max=5, reset_timeout=30). It
wraps vector-store retrieval in
indexing/vector_store.py
only.
LLM calls and SQL execution are not breaker-guarded. Their failures surface
as PipelineError values in graph state; see
Error Handling.
Adapters enforce row_limit and max_bytes on results
(see SDK Reference). These cap payload size; they are not
isolation.
| Mechanism | Module | What it bounds |
|---|---|---|
| AST validation + RBAC | pipeline/nodes/validator/node.py |
What SQL may be generated at all |
CancellationToken |
common/cancellation.py |
One run, cooperatively |
global_timeout_sec |
pipeline/runtime.py |
How long the caller waits |
VECTOR_BREAKER |
common/resilience.py |
Vector retrieval only |
ThreadPoolExecutor |
pipeline/runtime.py |
Concurrency, not isolation |
If you need blast-radius containment against driver-level crashes, run the engine in a process you are willing to lose (a per-request worker, a container replica) and rely on your supervisor to restart it. The engine does not provide that boundary itself.