High-level handler for interactive Python execution.
The Sandbox automates the start/resume loop by dispatching name lookups, function calls, dataclass method calls, and OS calls to handler callbacks.
External function names are auto-detected at runtime: when Python code
references an undefined name, the sandbox checks the :functions map and
:handler module to decide whether to provide a function object or raise
NameError. No upfront declaration of external functions is needed.
Dataclass method calls are dispatched through the same function handlers as
regular external function calls. The method name is looked up in the :functions
map or dispatched to handle_function/3 in the :handler module.
Module-based Handler
defmodule MyHandler do
@behaviour ExMonty.Sandbox
@impl true
def handle_function("fetch", [url], _kwargs) do
case Req.get(url) do
{:ok, resp} -> {:ok, resp.body}
{:error, _} -> {:error, :runtime_error, "fetch failed"}
end
end
@impl true
def handle_os(:read_text, [{:path, path}], _kwargs) do
case File.read(path) do
{:ok, content} -> {:ok, content}
{:error, reason} -> {:error, :file_not_found_error, to_string(reason)}
end
end
end
{:ok, result, output} = ExMonty.Sandbox.run(code,
inputs: %{"url" => "https://example.com"},
handler: MyHandler,
limits: %{max_duration_secs: 5.0}
)Function Map Handler
{:ok, result, output} = ExMonty.Sandbox.run(code,
inputs: %{"x" => 1},
functions: %{
"fetch" => fn [url], _kwargs -> {:ok, "response"} end
}
)Clock Handlers
date.today() and datetime.now() are surfaced as :date_today and
:datetime_now os calls. Provide handlers in the :os map to control what
"now" means (deterministic tests, time-travel, request timestamps, etc.):
now = DateTime.utc_now()
{:ok, _result, _} = ExMonty.Sandbox.run(
"from datetime import datetime, timezone\ndatetime.now(tz=timezone.utc).year",
os: %{
datetime_now: fn _args, _kwargs ->
{:ok, {:datetime, %{
year: now.year, month: now.month, day: now.day,
hour: now.hour, minute: now.minute, second: now.second,
microsecond: elem(now.microsecond, 0),
offset_seconds: 0, tz_name: nil
}}}
end
}
)Host Filesystem Mounts
For sandboxed access to real host directories, pass an ExMonty.Mount
as the :mounts option:
mounts =
ExMonty.Mount.new!()
|> ExMonty.Mount.add!("/data", "/var/lib/myapp/data", :read_only)
ExMonty.Sandbox.run(code, mounts: mounts):mounts composes with :os — mounts handle filesystem calls, the
:os map handles non-filesystem fallbacks (:getenv,
:datetime_now, etc.). See ExMonty.Mount for mode semantics, lease
lifecycle, and security guarantees.
Pseudo Filesystem
Pass an ExMonty.PseudoFS as the :os option for sandboxed filesystem access:
fs = ExMonty.PseudoFS.new()
|> ExMonty.PseudoFS.put_file("/data/input.txt", "hello world")
{:ok, result, output} = ExMonty.Sandbox.run(
"from pathlib import Path; Path('/data/input.txt').read_text()",
os: fs
)
Summary
Callbacks
Called when Python code invokes an external function.
Called when Python code references an undefined name.
Called when Python code performs an OS/filesystem operation.
Functions
Compiles and runs Python code with automatic handler dispatch.
Types
Callbacks
@callback handle_function(name :: String.t(), args :: list(), kwargs :: map()) :: handler_result()
Called when Python code invokes an external function.
Should return {:ok, value} on success or {:error, exc_type, message} on failure.
Called when Python code references an undefined name.
Return {:ok, value} to provide the value, or :undefined to raise NameError.
For external functions, return {:ok, {:function, name}}.
@callback handle_os(function :: atom(), args :: list(), kwargs :: map()) :: handler_result()
Called when Python code performs an OS/filesystem operation.
Optional — defaults to returning an error for all OS calls.
Functions
Compiles and runs Python code with automatic handler dispatch.
Options
:inputs- map of input variable names to values (default:%{}):handler- module implementingExMonty.Sandboxbehaviour:functions- map of function name strings to handler fns(args, kwargs -> result):os- OS call handler. Can be:- An
ExMonty.PseudoFSstruct for in-memory filesystem - A map of
%{atom => fn args, kwargs -> result}for per-function handlers
- An
:mounts- anExMonty.Mountfor host filesystem access. Composes with:os(mounts handle FS calls;:osmap handles non-FS fallback calls like:getenvor:datetime_now). Unmounted paths raisePermissionError.:limits- resource limits map (default:nil):script_name- script name for tracebacks (default:"main.py")
Either :handler or :functions must be provided for external function calls.
OS calls require either :os or handle_os/3 in the :handler module.
Examples
{:ok, result, output} = ExMonty.Sandbox.run(
"result = fetch('https://example.com')",
handler: MyHandler
)
{:ok, result, output} = ExMonty.Sandbox.run(
"result = double(21)",
functions: %{
"double" => fn [x], _kwargs -> {:ok, x * 2} end
}
)
# With pseudo filesystem
fs = ExMonty.PseudoFS.new()
|> ExMonty.PseudoFS.put_file("/config.json", ~s({"key": "value"}))
{:ok, result, output} = ExMonty.Sandbox.run(
~s(from pathlib import Path; Path('/config.json').read_text()),
os: fs
)