docs/adr/0003-worker-is-the-sandbox.md
The worker and the sandbox collapse into one unit. A worker polls one job at a time
(concurrency 1), resolves it, and runs the engine in its in-process sandbox box
(SANDBOX_CODE_ONLY: a node child + isolated-vm). There is no separate sandbox container, no
/execute HTTP hop, no Docker socket, no pool. Parallelism is horizontal: the operator runs N
worker replicas, each its own container capped at 0.5 CPU / 1 GB. This supersedes the short-lived
LOCAL_POOL / GCP_CLOUD_RUN exploration (worker-as-pool-manager over HTTP), which is removed.
Runtime seam's multiple kinds, runtime-factory, and the RuntimeKind enum.cloud-run-runtime + cloud-run-provisioner (GCP REST provisioning) and google-auth-library.local-pool-runtime + docker-provisioner + dockerode (worker-spawns-containers).sandbox-http-client and the sandbox /execute HTTP server (sandbox/src/server/main.ts) with its
ExecuteRequest/ExecuteResponse wire contract.createSandboxRuntime directly and pins concurrency to 1.The @activepieces/sandbox package stays — it is the in-process box (cache, sandbox-manager, fork /
isolate, resolver). It is imported and run by the worker, not shipped as its own container.
LOCAL_POOL model needed — the worker no longer drives
the Docker daemon, so there is nothing privileged to mount and the self-hosting story is just "scale
workers", which is how Activepieces already scales. (This was the biggest risk in the prior model.)AP_WORKER_CONCURRENCY defaults to 1 and the worker runs a
single poll loop regardless.isolated-vm + bun +
esbuild) plus the worker's own deps — including the heavy ai-sdk cluster behind the chat agent. To
keep it small: the worker entry is esbuild-bundled into one file (Dockerfile.worker, multi-stage,
no workspace node_modules in the final image, same trick as the engine), and the chat-agent
handler is lazy-loaded (import() on first chat job) so its dependency graph never evaluates in a
flow-only worker — keeping idle RSS small.