Packages
Secure BEAM sandbox runtime for LLM code mode and MCP aggregation. Run concurrent LLM/tool clients safely while agents orchestrate approved tools, call upstream MCP/OpenAPI servers, and transform data.
Current section
Files
Jump to
Current section
Files
lib/ptc_runner/sandbox.ex
defmodule PtcRunner.Sandbox do
@moduledoc """
Executes programs in isolated BEAM processes with resource limits.
Spawns isolated processes with configurable timeout and memory limits,
ensuring safe program execution.
## Resource Limits
| Resource | Default | Option |
|----------|---------|--------|
| Timeout | 1,000 ms | `:timeout` |
| Max Heap | ~10 MB (1,250,000 words) | `:max_heap` |
## Configuration
Limits can be set per-call:
PtcRunner.Json.run(program, timeout: 5000, max_heap: 5_000_000)
Or as application-level defaults in `config.exs`:
config :ptc_runner,
default_timeout: 2000,
default_max_heap: 2_500_000
"""
alias PtcRunner.Context
alias PtcRunner.Json.Interpreter
@typedoc """
Execution metrics for a program run.
"""
@type metrics :: %{
duration_ms: integer(),
memory_bytes: integer()
}
@doc """
Executes an AST in an isolated sandbox process.
## Arguments
- ast: The AST to execute
- context: The execution context
- opts: Options (timeout, max_heap)
## Returns
- `{:ok, result, metrics, memory}` on success
- `{:error, reason}` on failure
"""
@spec execute(map(), Context.t(), keyword()) ::
{:ok, any(), metrics(), map()}
| {:error, {atom(), non_neg_integer()} | {atom(), String.t()}}
def execute(ast, context, opts \\ []) do
timeout = Keyword.get(opts, :timeout, 1000)
max_heap = Keyword.get(opts, :max_heap, 1_250_000)
# Spawn isolated process with resource limits
start_time = System.monotonic_time(:millisecond)
parent = self()
{pid, ref} =
Process.spawn(
fn ->
# Set process priority to normal within the process
Process.flag(:priority, :normal)
result = Interpreter.eval(ast, context)
memory = get_process_memory()
send(parent, {:result, result, memory})
end,
[:monitor, {:max_heap_size, max_heap}]
)
# Wait for result with timeout
receive do
{:result, result, memory} ->
end_time = System.monotonic_time(:millisecond)
duration = end_time - start_time
Process.demonitor(ref, [:flush])
case result do
{:ok, value, eval_memory} ->
{:ok, value, %{duration_ms: duration, memory_bytes: memory}, eval_memory}
{:error, reason} ->
{:error, reason}
end
{:DOWN, ^ref, :process, ^pid, :killed} ->
{:error, {:memory_exceeded, max_heap * 8}}
{:DOWN, ^ref, :process, ^pid, reason} ->
{:error, {:execution_error, "Process terminated: #{inspect(reason)}"}}
after
timeout ->
# Kill the process if it times out
Process.demonitor(ref, [:flush])
Process.exit(pid, :kill)
{:error, {:timeout, timeout}}
end
end
defp get_process_memory do
case Process.info(self(), :memory) do
{:memory, bytes} -> bytes
nil -> 0
end
end
end