mehmetsekercioglu
Hello,
I’ve been building Zygo, a Linux sandbox runtime for running code other people wrote: customer plugins, workflow steps, an LLM’s tool calls. zygo_sdk is its Elixir client.
Why a BEAM app might want it
If your app has to run a user’s Python or JavaScript, the BEAM can’t sandbox it for you. A Port or System.cmd runs it as your app’s own user, with your files and your network. Zygo gives every call its own process, cgroup, deadline and secrets, inside a sandbox (namespaces, seccomp, Landlock, a read-only root from an OCI image, network off unless you allow names), and throws it away afterwards. For Python it forks a warm, pre-imported interpreter, so a call costs about 1.4 ms through the API on a 2 vCPU Linux VM. Node can’t be forked safely, so each JS call gets a pre-loaded worker instead, at about 25 ms of CPU.
What it looks like
client = Zygo.connect()
Zygo.serve_runtime!(client, "py312", %{"image" => "python:3.12-slim", "agent" => "python"})
script = Zygo.put_script!(client, """
def handler(event):
words = event["text"].split()
return {"words": len(words), "longest": max(words, key=len)}
""")
Zygo.run_script!(client, "py312", script.sha256, %{"text" => "the quick brown fox"}).result
#=> %{"words" => 4, "longest" => "quick"}
A script that misbehaves stays in its sandbox:
{:error, %Zygo.Error{kind: :handler, stderr: stderr}} =
Zygo.run_script(client, "py312", "def handler(e):\n open('/etc/shadow').read()\n")
The client
- Every API call returns
{:ok, value}or{:error, %Zygo.Error{}}and has a!twin. One error struct with akind(:busy,:handler,:timeout, …) instead of a module per error. - Pooled keep-alive connections over TCP or a unix socket, with a named pool you start under your own supervisor.
:busyand:unavailablemean the request never ran, so they can be retried for you, honouringRetry-After.- A call’s output can be streamed as a lazy
Stream. - Two dependencies, Mint and NimblePool. Elixir 1.18+, for the standard library’s JSON.
Before you try it
The package is only a client. zygo itself is a static binary for Linux; on macOS it runs in a VM it manages. The README’s “Before you start” section has the four commands to get the API running.
Limits
The wall is the host kernel, so it’s meant for semi-trusted code, your customers’ rather than anonymous attackers’. The escape suite attempts 21 vectors with 0 escapes, but there has been no external audit. It’s Linux only and one machine. v0.1.5, Apache-2.0, one maintainer, no production users yet.
Feedback I’d especially like
- Is one error struct with a
kindthe right shape for Elixir, or would you rather match on exception modules? ADR 0007 explains why I chose it. - How would you supervise this in a real app: one named pool per API, or one per tenant?
- Anything that feels unidiomatic.
Links:
- Hex: zygo_sdk | Hex
- Docs: Zygo v0.1.5 — Documentation
- The API and the clients: zygo/docs/book/17-api-sdk-mcp.md at main · mhmtskrc2/zygo · GitHub
- ADR 0007, why the client looks this way: zygo/docs/book/adr/0007-elixir-sdk.md at main · mhmtskrc2/zygo · GitHub
- Source:
I built it with heavy use of Claude Code.
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ai
- #ecto-query
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #elixirconf-eu
- #api
- #forms
- #metaprogramming
- #hex









