4-Layer Safe Tool & Process Execution Sandbox (REQ-SAFE-01..REQ-SAFE-03, REQ-PBT-05).
Guarantees that tool execution NEVER crashes the calling :gen_statem conversation process or
the BEAM VM:
- Layer 1 (BEAM Process & Heap Isolation): Runs every tool function inside an unlinked
Task.Supervisor.async_nolink(SuperPi.ToolTaskSupervisor, ...)process with a configurable:max_heap_sizeflag (%{size: max_heap_words, kill: true, error_logger: true}). If a tool raises an exception, callsexit/1, times out, or exceeds:max_heap_size, the supervisor catches the:DOWNmessage and returns{:error, %{type: ..., reason: ...}}. - Layer 2 (Backpressure OS Process Execution + 4 MiB Spill Files): Executes external commands
with streaming output capture and automatically spills outputs exceeding
4 MiB(4_194_304bytes) to temporary files while returning a bounded preview.
Summary
Functions
Executes an external OS command safely using Exile streaming with backpressure and 4 MiB spill-to-disk
protection (REQ-SAFE-02).
Runs fun.() inside an unlinked supervised task with :max_heap_size enforcement and a strict timeout.
Never raises or crashes the caller.
Starts fun as an unlinked task under SuperPi.ToolTaskSupervisor that does not outlive its
caller: a watcher process kills the task when the caller exits. A task that runs an OS command
sends {:os_pid, pid} to the watcher (an arity-1 fun gets the watcher pid), and the watcher
kills that process tree first. The caller receives {ref, result} or {:DOWN, ref, ...}, as
with Task.Supervisor.async_nolink/2.
Functions
@spec exec_command(String.t(), [String.t()], keyword()) :: {:ok, %{exit_code: integer(), output: String.t(), spilled_path: String.t() | nil}} | {:error, map()}
Executes an external OS command safely using Exile streaming with backpressure and 4 MiB spill-to-disk
protection (REQ-SAFE-02).
Runs fun.() inside an unlinked supervised task with :max_heap_size enforcement and a strict timeout.
Never raises or crashes the caller.
Starts fun as an unlinked task under SuperPi.ToolTaskSupervisor that does not outlive its
caller: a watcher process kills the task when the caller exits. A task that runs an OS command
sends {:os_pid, pid} to the watcher (an arity-1 fun gets the watcher pid), and the watcher
kills that process tree first. The caller receives {ref, result} or {:DOWN, ref, ...}, as
with Task.Supervisor.async_nolink/2.
Nested tasks chain: cancelling a conversation kills its tool task, which ends the sandbox task under it, which ends the command and its OS processes.